Skip to content

Commit

Permalink
Fix Repair-WinGetPackageManager cmdlet by retrieving dependencies fro…
Browse files Browse the repository at this point in the history
…m GitHub assets (#4923)
  • Loading branch information
ryfu-msft committed Oct 31, 2024
1 parent ede0074 commit cd2ebc1
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public void ResetAllSources()
private WinGetCLICommandResult Run(string command, string parameters, int timeOut = 60000)
{
var wingetCliWrapper = new WingetCLIWrapper();
var result = wingetCliWrapper.RunCommand(command, parameters, timeOut);
var result = wingetCliWrapper.RunCommand(this, command, parameters, timeOut);
result.VerifyExitCode();

return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="UserSettingsCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand Down Expand Up @@ -42,7 +42,7 @@ public UserSettingsCommand(PSCmdlet psCmdlet)
if (winGetSettingsFilePath == null)
{
var wingetCliWrapper = new WingetCLIWrapper();
var settingsResult = wingetCliWrapper.RunCommand("settings", "export");
var settingsResult = wingetCliWrapper.RunCommand(this, "settings", "export");

// Read the user settings file property.
var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="VersionCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand Down Expand Up @@ -30,7 +30,7 @@ public VersionCommand(PSCmdlet psCmdlet)
/// </summary>
public void Get()
{
this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion.TagVersion);
this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion(this).TagVersion);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers

private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, bool allUsers, bool force)
{
var installedVersion = WinGetVersion.InstalledWinGetVersion;
var installedVersion = WinGetVersion.InstalledWinGetVersion(this);
bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0;

string message = $"Installed WinGet version '{installedVersion.TagVersion}' " +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="WinGetIntegrity.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand Down Expand Up @@ -44,7 +44,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers
// 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");
var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version");
result.VerifyExitCode();
}
catch (Win32Exception e)
Expand All @@ -68,7 +68,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers
{
// This assumes caller knows that the version exist.
WinGetVersion expectedWinGetVersion = new WinGetVersion(expectedVersion);
var installedVersion = WinGetVersion.InstalledWinGetVersion;
var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet);
if (expectedWinGetVersion.CompareTo(installedVersion) != 0)
{
throw new WinGetIntegrityException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="ReleaseExtensions.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand All @@ -24,16 +24,28 @@ internal static class ReleaseExtensions
/// <returns>The asset.</returns>
public static ReleaseAsset GetAsset(this Release release, string name)
{
var assets = release.Assets.Where(a => a.Name == name);
var asset = TryGetAsset(release, name);

if (assets.Any())
if (asset != null)
{
return assets.First();
return asset;
}

throw new WinGetRepairException(string.Format(Resources.ReleaseAssetNotFound, name));
}

/// <summary>
/// Gets the Asset if present.
/// </summary>
/// <param name="release">GitHub release.</param>
/// <param name="name">Name of asset.</param>
/// <returns>The asset, or null if not found.</returns>
public static ReleaseAsset? TryGetAsset(this Release release, string name)
{
var assets = release.Assets.Where(a => a.Name == name);
return assets.Any() ? assets.First() : null;
}

/// <summary>
/// Gets the asset that ends with the string.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ namespace Microsoft.WinGet.Client.Engine.Helpers
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.WinGet.Client.Engine.Common;
using Microsoft.WinGet.Client.Engine.Exceptions;
using Microsoft.WinGet.Client.Engine.Extensions;
using Microsoft.WinGet.Common.Command;
using Newtonsoft.Json;
using Octokit;
using Semver;
using static Microsoft.WinGet.Client.Engine.Common.Constants;
Expand Down Expand Up @@ -63,8 +67,13 @@ internal class AppxModuleHelper

// Assets
private const string MsixBundleName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle";
private const string DependenciesJsonName = "DesktopAppInstaller_Dependencies.json";
private const string DependenciesZipName = "DesktopAppInstaller_Dependencies.zip";
private const string License = "License1.xml";

// Format of a dependency package such as 'x64\Microsoft.VCLibs.140.00.UWPDesktop_14.0.33728.0_x64.appx'
private const string ExtractedDependencyPath = "{0}\\{1}_{2}_{0}.appx";

// Dependencies
// VCLibs
private const string VCLibsUWPDesktop = "Microsoft.VCLibs.140.00.UWPDesktop";
Expand Down Expand Up @@ -319,53 +328,67 @@ private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade,

private async Task InstallDependenciesAsync(string releaseTag)
{
// 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.
await this.InstallVCLibsDependenciesAsync();
await this.InstallUiXamlAsync(releaseTag);
bool result = await this.InstallDependenciesFromGitHubArchive(releaseTag);

if (!result)
{
// 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 method no longer works for versions >1.9 as the vclibs url has been deprecated.
await this.InstallVCLibsDependenciesFromUriAsync();
await this.InstallUiXamlAsync(releaseTag);
}
}

private async Task InstallVCLibsDependenciesAsync()
private Dictionary<string, string> GetDependenciesByArch(PackageDependency dependencies)
{
var result = this.ExecuteAppxCmdlet(
GetAppxPackage,
new Dictionary<string, object>
{
{ Name, VCLibsUWPDesktop },
});
Dictionary<string, string> appxPackages = new Dictionary<string, string>();
var arch = RuntimeInformation.OSArchitecture;

// See if the minimum (or greater) version is installed.
// TODO: Pull the minimum version from the target package
Version minimumVersion = new Version(VCLibsUWPDesktopVersion);
string appxPackageX64 = string.Format(ExtractedDependencyPath, "x64", dependencies.Name, dependencies.Version);
string appxPackageX86 = string.Format(ExtractedDependencyPath, "x86", dependencies.Name, dependencies.Version);
string appxPackageArm = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version);
string appxPackageArm64 = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version);

// Construct the list of frameworks that we want present.
Dictionary<string, string> vcLibsDependencies = new Dictionary<string, string>();
var arch = RuntimeInformation.OSArchitecture;
if (arch == Architecture.X64)
{
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
appxPackages.Add("x64", appxPackageX64);
}
else if (arch == Architecture.X86)
{
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
appxPackages.Add("x86", appxPackageX86);
}
else if (arch == Architecture.Arm64)
{
// Deployment please figure out for me.
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
vcLibsDependencies.Add("arm", VCLibsUWPDesktopArm);
vcLibsDependencies.Add("arm64", VCLibsUWPDesktopArm64);
appxPackages.Add("x64", appxPackageX64);
appxPackages.Add("x86", appxPackageX86);
appxPackages.Add("arm", appxPackageArm);
appxPackages.Add("arm64", appxPackageArm64);
}
else
{
throw new PSNotSupportedException(arch.ToString());
}

return appxPackages;
}

private void FindMissingDependencies(Dictionary<string, string> dependencies, string packageName, string requiredVersion)
{
var result = this.ExecuteAppxCmdlet(
GetAppxPackage,
new Dictionary<string, object>
{
{ Name, packageName },
});

Version minimumVersion = new Version(requiredVersion);

if (result != null &&
result.Count > 0)
{
Expand All @@ -384,24 +407,30 @@ private async Task InstallVCLibsDependenciesAsync()
string? architectureString = psobject?.Architecture?.ToString();
if (architectureString == null)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs dependency has no architecture value: {psobject?.PackageFullName ?? "<null>"}");
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} dependency has no architecture value: {psobject?.PackageFullName ?? "<null>"}");
continue;
}

architectureString = architectureString.ToLower();

if (vcLibsDependencies.ContainsKey(architectureString))
if (dependencies.ContainsKey(architectureString))
{
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs {architectureString} dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}");
vcLibsDependencies.Remove(architectureString);
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} {architectureString} dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}");
dependencies.Remove(architectureString);
}
}
else
{
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs is lower than minimum required version [{minimumVersion}]: {psobject?.PackageFullName ?? "<null>"}");
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} is lower than minimum required version [{minimumVersion}]: {psobject?.PackageFullName ?? "<null>"}");
}
}
}
}

private async Task InstallVCLibsDependenciesFromUriAsync()
{
Dictionary<string, string> vcLibsDependencies = this.GetVCLibsDependencies();
this.FindMissingDependencies(vcLibsDependencies, VCLibsUWPDesktop, VCLibsUWPDesktopVersion);

if (vcLibsDependencies.Count != 0)
{
Expand All @@ -419,6 +448,108 @@ private async Task InstallVCLibsDependenciesAsync()
}
}

// Returns a boolean value indicating whether dependencies were successfully installed from the GitHub release assets.
private async Task<bool> InstallDependenciesFromGitHubArchive(string releaseTag)
{
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
var release = await githubClient.GetReleaseAsync(releaseTag);

ReleaseAsset? dependenciesJsonAsset = release.TryGetAsset(DependenciesJsonName);
if (dependenciesJsonAsset is null)
{
return false;
}

using var dependenciesJsonFile = new TempFile();
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesJsonAsset.BrowserDownloadUrl, dependenciesJsonFile.FullPath, this.pwshCmdlet);

using StreamReader r = new StreamReader(dependenciesJsonFile.FullPath);
string json = r.ReadToEnd();
WingetDependencies? wingetDependencies = JsonConvert.DeserializeObject<WingetDependencies>(json);

if (wingetDependencies is null)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to deserialize dependencies json file.");
return false;
}

List<string> missingDependencies = new List<string>();
foreach (var dependency in wingetDependencies.Dependencies)
{
Dictionary<string, string> dependenciesByArch = this.GetDependenciesByArch(dependency);
this.FindMissingDependencies(dependenciesByArch, dependency.Name, dependency.Version);

foreach (var pair in dependenciesByArch)
{
missingDependencies.Add(pair.Value);
}
}

if (missingDependencies.Count != 0)
{
using var dependenciesZipFile = new TempFile();
using var extractedDirectory = new TempDirectory();

ReleaseAsset? dependenciesZipAsset = release.TryGetAsset(DependenciesZipName);
if (dependenciesZipAsset is null)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Dependencies zip asset not found on GitHub asset.");
return false;
}

await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesZipAsset.BrowserDownloadUrl, dependenciesZipFile.FullPath, this.pwshCmdlet);
ZipFile.ExtractToDirectory(dependenciesZipFile.FullPath, extractedDirectory.FullDirectoryPath);

foreach (var entry in missingDependencies)
{
string fullPath = System.IO.Path.Combine(extractedDirectory.FullDirectoryPath, entry);
if (!File.Exists(fullPath))
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Package dependency not found in archive: {fullPath}");
return false;
}

_ = this.ExecuteAppxCmdlet(
AddAppxPackage,
new Dictionary<string, object>
{
{ Path, fullPath },
{ ErrorAction, Stop },
});
}
}

return true;
}

private Dictionary<string, string> GetVCLibsDependencies()
{
Dictionary<string, string> vcLibsDependencies = new Dictionary<string, string>();
var arch = RuntimeInformation.OSArchitecture;
if (arch == Architecture.X64)
{
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
}
else if (arch == Architecture.X86)
{
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
}
else if (arch == Architecture.Arm64)
{
// Deployment please figure out for me.
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
vcLibsDependencies.Add("arm", VCLibsUWPDesktopArm);
vcLibsDependencies.Add("arm64", VCLibsUWPDesktopArm64);
}
else
{
throw new PSNotSupportedException(arch.ToString());
}

return vcLibsDependencies;
}

private async Task InstallUiXamlAsync(string releaseTag)
{
(string xamlPackageName, string xamlReleaseTag) = GetXamlDependencyVersionInfo(releaseTag);
Expand Down
Loading

0 comments on commit cd2ebc1

Please sign in to comment.