diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 01daa34cf6..d6900d0f08 100644 --- 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 index d080513cd7..9298d76fbf 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs @@ -21,6 +21,12 @@ namespace Microsoft.WinGet.Client.Commands [OutputType(typeof(int))] public class RepairWinGetPackageManagerCmdlet : WinGetPackageManagerCmdlet { + /// + /// Gets or sets a value indicating whether to repair for all users. Requires admin. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllUsers { get; set; } + /// /// Attempts to repair winget. /// TODO: consider WhatIf and Confirm options. @@ -30,11 +36,11 @@ protected override void ProcessRecord() 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 index 722ab72ce6..a6d534449d 100644 --- 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; /// /// 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" }; /// /// Initializes a new instance of the class. @@ -39,8 +38,8 @@ public WinGetPackageManagerCommand(PSCmdlet psCmdlet) /// Use prerelease version on GitHub. 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 @@ public void Assert(string expectedVersion) /// Repairs winget using the latest version on winget-cli. /// /// Use prerelease version on GitHub. - public void RepairUsingLatest(bool preRelease) + /// Install for all users. Requires admin. + 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); } /// /// Repairs winget if needed. /// /// The expected version, if any. - public void Repair(string expectedVersion) + /// Install for all users. Requires admin. + 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(); + + 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 index c1a14153cd..f117bfeece 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs @@ -33,5 +33,32 @@ internal static class Constants /// Name of PATH environment variable. /// public const string PathEnvVar = "PATH"; + + /// + /// Repository owners. + /// + public class RepositoryOwner + { + /// + /// Microsoft org. + /// + public const string Microsoft = "microsoft"; + } + + /// + /// Repository names. + /// + public class RepositoryName + { + /// + /// https://github.com/microsoft/winget-cli . + /// + public const string WinGetCli = "winget-cli"; + + /// + /// https://github.com/microsoft/microsoft-ui-xaml . + /// + 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 index aedb573190..b026070109 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs @@ -60,5 +60,10 @@ public enum IntegrityCategory /// Installed App Installer package is not supported. /// AppInstallerNotSupported, + + /// + /// No applicable license found. + /// + AppInstallerNoLicense, } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs index ff9e99aabe..62df3098a3 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -74,35 +74,34 @@ public static void AssertWinGet(PSCmdlet psCmdlet, string expectedVersion) IntegrityCategory.UnexpectedVersion, string.Format( Resources.IntegrityUnexpectedVersionMessage, - installedVersion, + installedVersion.TagVersion, expectedVersion)); } } } - /// - /// Verifies winget runs correctly. - /// - /// The calling cmdlet. - /// Expected version. - /// Integrity category. - 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 index 6b07d46bd1..fe716d172d 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs @@ -62,6 +62,7 @@ public WinGetIntegrityException(IntegrityCategory category, string message) 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 new file mode 100644 index 0000000000..3334ca8d63 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairException.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Properties; + + /// + /// WinGet repair exception. + /// + [Serializable] + public class WinGetRepairException : RuntimeException + { + /// + /// Initializes a new instance of the class. + /// + /// Integrity exception. + public WinGetRepairException(WinGetIntegrityException ie) + : base(GetMessage(ie), ie) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Inner exception. + public WinGetRepairException(Exception e) + : base(Resources.RepairFailureMessage, e) + { + } + + /// + /// Initializes a new instance of the class. + /// + public WinGetRepairException() + : base(Resources.RepairFailureMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Message.. + 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 index 582e955fb9..ecd1c68179 100644 --- 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; /// /// Helper to make calls to the Appx module. @@ -23,17 +23,21 @@ internal class AppxModuleHelper 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 @@ internal class AppxModuleHelper 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 @@ internal class AppxModuleHelper 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; @@ -96,46 +111,6 @@ public string GetAppInstallerPropertyValue(string propertyName) 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(); - - var options = new List(); - if (downgrade) - { - options.Add(ForceUpdateFromAnyVersion); - } - - try - { - _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary - { - { Path, localPath }, - { ErrorAction, Stop }, - }, - options); - } - catch (RuntimeException e) - { - this.psCmdlet.WriteError(e.ErrorRecord); - throw e; - } - } - /// /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml. /// @@ -160,6 +135,93 @@ public void RegisterAppInstaller() }); } + /// + /// Install AppInstaller's bundle from a GitHub release. + /// + /// Release tag of GitHub release. + /// If install for all users is needed. + /// Is downgrade. + 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(); + 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 @@ private PSObject GetAppxObject(string packageName) .FirstOrDefault(); } - private IReadOnlyList GetVCLibsDependencies() + private void InstallDependencies() { - var vcLibsDependencies = new List(); + // 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 @@ -200,6 +272,8 @@ private IReadOnlyList GetVCLibsDependencies() if (!isInstalled) { this.psCmdlet.WriteDebug("Couldn't find required VCLibs package"); + + var vcLibsDependencies = new List(); var arch = RuntimeInformation.OSArchitecture; if (arch == Architecture.X64) { @@ -221,46 +295,69 @@ private IReadOnlyList GetVCLibsDependencies() { 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(); + 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 options = null) { try { _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary - { - { Path, packageUri }, - { ErrorAction, Stop }, - }); + AddAppxPackage, + new Dictionary + { + { Path, packageUri }, + { ErrorAction, Stop }, + }, + options); } catch (RuntimeException e) { @@ -268,7 +365,7 @@ private void AddAppxPackageAsUri(string packageUri) 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 @@ private void AddAppxPackageAsUri(string packageUri) } } - private void DownloadPackageAndAdd(string packageUrl) + private void DownloadPackageAndAdd(string packageUrl, IList 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 - { - { Path, tempFile.FullPath }, - { ErrorAction, Stop }, - }); + AddAppxPackage, + new Dictionary + { + { Path, tempFile.FullPath }, + { ErrorAction, Stop }, + }, + options); } private Collection ExecuteAppxCmdlet(string cmdlet, Dictionary parameters = null, IList options = null) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs similarity index 64% rename from src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs rename to src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs index b8da873beb..56b295a814 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -1,5 +1,5 @@ // ----------------------------------------------------------------------------- -// +// // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // // ----------------------------------------------------------------------------- @@ -9,40 +9,53 @@ 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; /// - /// Handles WinGet's releases in GitHub. + /// Handles GitHub interactions. /// - internal class GitHubRelease + internal class GitHubClient { - 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 string owner; + private readonly string repo; + private readonly IGitHubClient gitHubClient; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public GitHubRelease() + /// Owner. + /// Repository. + public GitHubClient(string owner, string repo) { - this.gitHubClient = new GitHubClient(new ProductHeaderValue(UserAgent)); + this.gitHubClient = new Octokit.GitHubClient(new ProductHeaderValue(UserAgent)); + this.owner = owner; + this.repo = repo; } /// - /// Download a release from winget-cli. + /// Gets a release. /// - /// Optional release name. If null, gets latest. - /// Output file. - public void DownloadRelease(string releaseTag, string outputFile) + /// Release tag. + /// The Release. + public Release GetRelease(string releaseTag) { - this.DownloadReleaseAsync(releaseTag, outputFile).GetAwaiter().GetResult(); + return this.GetReleaseAsync(releaseTag).GetAwaiter().GetResult(); + } + + /// + /// Gets a release. + /// + /// Release tag. + /// The Release. + public async Task GetReleaseAsync(string releaseTag) + { + return await this.gitHubClient.Repository.Release.Get(this.owner, this.repo, releaseTag); } /// @@ -65,22 +78,6 @@ 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. - /// Output file. - /// A representing the asynchronous operation. - 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); - } - /// /// Downloads a file from a url. /// @@ -113,11 +110,11 @@ internal async Task GetLatestVersionAsync(bool includePreRelease) if (includePreRelease) { // GetAll orders by newest and includes pre releases. - release = (await this.gitHubClient.Repository.Release.GetAll(Owner, Repo))[0]; + release = (await this.gitHubClient.Repository.Release.GetAll(this.owner, this.repo))[0]; } else { - release = await this.gitHubClient.Repository.Release.GetLatest(Owner, Repo); + release = await this.gitHubClient.Repository.Release.GetLatest(this.owner, this.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 new file mode 100644 index 0000000000..b9a94bc26b --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempDirectory.cs @@ -0,0 +1,118 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.IO; + + /// + /// Creates a temporary directory in the user's temporary directory. + /// + internal class TempDirectory : IDisposable + { + private readonly bool cleanup; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// Optional directory name. If null, creates a random directory name. + /// Delete directory if already exists. Default true. + /// Deletes directory at disposing time. Default true. + 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; + } + + /// + /// Gets the directory name. + /// + public string DirectoryName { get; } + + /// + /// Gets the full directory name. + /// + public string FullDirectoryPath { get; } + + /// + /// IDisposable.Dispose . + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Copies all contents of a directory into this directory. + /// + /// Source directory. + public void CopyDirectory(string sourceDir) + { + this.CopyDirectory(sourceDir, this.FullDirectoryPath); + } + + /// + /// Protected disposed. + /// + /// Disposing. + 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 index 0930d8692b..c00d94f95c 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 @@ -22,7 +22,7 @@ - USE_PROD_CLSIDS + $(DefineConstants);USE_PROD_CLSIDS @@ -118,7 +118,7 @@ - POWERSHELL_WINDOWS + $(DefineConstants);POWERSHELL_WINDOWS diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs index 5eb99f1123..ecf63b656e 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -60,15 +60,6 @@ internal Resources() { } } - /// - /// 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.. - /// - internal static string AppExecutionAliasDisabledHelpMessage { - get { - return ResourceManager.GetString("AppExecutionAliasDisabledHelpMessage", resourceCulture); - } - } - /// /// Looks up a localized string similar to An error occurred while connecting to the catalog.. /// @@ -96,6 +87,15 @@ internal static string IntegrityAppExecutionAliasDisabledMessage { } } + /// + /// Looks up a localized string similar to No applicable license found.. + /// + internal static string IntegrityAppInstallerLicense { + get { + return ResourceManager.GetString("IntegrityAppInstallerLicense", resourceCulture); + } + } + /// /// Looks up a localized string similar to The App Installer is not installed.. /// @@ -231,6 +231,42 @@ internal static string ProgressRecordActivityUpdating { } } + /// + /// Looks up a localized string similar to Try running with -AllUsers in administrator mode.. + /// + internal static string RepairAllUsersHelpMessage { + get { + return ResourceManager.GetString("RepairAllUsersHelpMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to -AllUsers requires administrator mode.. + /// + internal static string RepairAllUsersMessage { + get { + return ResourceManager.GetString("RepairAllUsersMessage", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string RepairAppExecutionAliasMessage { + get { + return ResourceManager.GetString("RepairAppExecutionAliasMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to repair winget.. + /// + internal static string RepairFailureMessage { + get { + return ResourceManager.GetString("RepairFailureMessage", resourceCulture); + } + } + /// /// 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).. /// @@ -259,7 +295,7 @@ internal static string VagueCriteriaExceptionMessage { } /// - /// 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.. /// 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 index 02cdf2f628..5e5cc9ad4c 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -186,7 +186,7 @@ The App Installer is not registered. - + 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. @@ -207,4 +207,18 @@ This cmdlet is not supported in Windows PowerShell. + + No applicable license found. + + + Try running with -AllUsers in administrator mode. + {Locked="-AllUsers"} + + + -AllUsers requires administrator mode. + {Locked="-AllUsers"} + + + Failed to repair winget. + \ No newline at end of file