Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Repair-WinGetPackageManager cmdlet by retrieving dependencies from GitHub assets #4923

Merged
merged 7 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
it works!
  • Loading branch information
ryfu-msft committed Oct 30, 2024
commit 5e9388753a47480bdde308155ad47b2e2bb82cfb
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 @@ -122,7 +122,6 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers

if (seenCategories.Contains(currentCategory))
{
this.Write(StreamType.Verbose, $"{currentCategory} encountered previously");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove this output?

throw;
}

Expand Down Expand Up @@ -169,7 +168,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 @@ -72,6 +72,9 @@ internal class AppxModuleHelper
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}_{3}.appx";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private const string ExtractedDependencyPath = "{0}\\{1}_{2}_{3}.appx";
private const string ExtractedDependencyPath = "{0}\\{1}_{2}_{0}.appx";

nit: I guess this isn't really that important, it just seems to suggest that the directory architecture and the file name architecture don't need to match.

If taken, update the format calls to remove the second architecture.


// Dependencies
// VCLibs
private const string VCLibsUWPDesktop = "Microsoft.VCLibs.140.00.UWPDesktop";
Expand All @@ -81,11 +84,7 @@ 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 AppxPackageTemplate = "{0}_{1}_{2}__8wekyb3d8bbwe.appx";

// Xaml
private const string XamlPackageName = "Microsoft.UI.Xaml.2.8";

private const string XamlPackage28 = "Microsoft.UI.Xaml.2.8";
private const string XamlReleaseTag286 = "v2.8.6";
private const string MinimumWinGetReleaseTagForXaml28 = "v1.7.10514";
Expand Down Expand Up @@ -330,7 +329,7 @@ private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade,

private async Task InstallDependenciesAsync(string releaseTag)
{
bool result = await this.TryInstallDependenciesFromGitHubArchive(releaseTag);
bool result = await this.InstallDependenciesFromGitHubArchive(releaseTag);

if (!result)
{
Expand All @@ -351,10 +350,10 @@ private Dictionary<string, string> GetDependenciesByArch(PackageDependency depen
Dictionary<string, string> appxPackages = new Dictionary<string, string>();
var arch = RuntimeInformation.OSArchitecture;

string appxPackageX64 = string.Format(AppxPackageTemplate, dependencies.Name, dependencies.Version, "x64");
string appxPackageX86 = string.Format(AppxPackageTemplate, dependencies.Name, dependencies.Version, "x86");
string appxPackageArm = string.Format(AppxPackageTemplate, dependencies.Name, dependencies.Version, "arm");
string appxPackageArm64 = string.Format(AppxPackageTemplate, dependencies.Name, dependencies.Version, "arm64");
string appxPackageX64 = string.Format(ExtractedDependencyPath, "x64", dependencies.Name, dependencies.Version, "x64");
string appxPackageX86 = string.Format(ExtractedDependencyPath, "x86", dependencies.Name, dependencies.Version, "x86");
string appxPackageArm = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version, "arm");
string appxPackageArm64 = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version, "arm64");

if (arch == Architecture.X64)
{
Expand Down Expand Up @@ -389,8 +388,6 @@ private void FindMissingDependencies(Dictionary<string, string> dependencies, st
{ Name, packageName },
});

// See if the minimum (or greater) version is installed.
// TODO: Pull the minimum version from the target package
Version minimumVersion = new Version(requiredVersion);

if (result != null &&
Expand Down Expand Up @@ -452,79 +449,78 @@ private async Task InstallVCLibsDependenciesFromUriAsync()
}
}

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

ReleaseAsset? dependenciesJsonAsset = release.TryGetAsset(DependenciesJsonName);
if (dependenciesJsonAsset is null)
{
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
var release = await githubClient.GetReleaseAsync(releaseTag);
return false;
}

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

var dependenciesJsonAsset = release.GetAsset(DependenciesJsonName);
var dependenciesZipAsset = release.GetAsset(DependenciesZipName);
using StreamReader r = new StreamReader(dependenciesJsonFile.FullPath);
string json = r.ReadToEnd();
WingetDependencies? wingetDependencies = JsonConvert.DeserializeObject<WingetDependencies>(json);

using var dependenciesJsonFile = new TempFile();
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesJsonAsset.BrowserDownloadUrl, dependenciesJsonFile.FullPath, this.pwshCmdlet);
if (wingetDependencies is null)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to deserialize dependencies json file.");
return false;
}

using StreamReader r = new StreamReader(dependenciesJsonFile.FullPath);
string json = r.ReadToEnd();
WingetDependencies? wingetDependencies = JsonConvert.DeserializeObject<WingetDependencies>(json);
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);

if (wingetDependencies is null)
foreach (var pair in dependenciesByArch)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to deserialize dependencies json file.");
return false;
missingDependencies.Add(pair.Value);
}
}

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);
if (missingDependencies.Count != 0)
{
using var dependenciesZipFile = new TempFile();
using var extractedDirectory = new TempDirectory();

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

if (missingDependencies.Count != 0)
{
using var dependenciesZipFile = new TempFile();
using var extractedDirectory = new TempDirectory();
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesZipAsset.BrowserDownloadUrl, dependenciesZipFile.FullPath, this.pwshCmdlet);
ZipFile.ExtractToDirectory(dependenciesZipFile.FullPath, extractedDirectory.FullDirectoryPath);
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesZipAsset.BrowserDownloadUrl, dependenciesZipFile.FullPath, this.pwshCmdlet);
ZipFile.ExtractToDirectory(dependenciesZipFile.FullPath, extractedDirectory.FullDirectoryPath);

foreach (var entry in missingDependencies)
foreach (var entry in missingDependencies)
{
string fullPath = System.IO.Path.Combine(extractedDirectory.FullDirectoryPath, DependenciesAssetName, entry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this directory ends up in the path, unless you are expecting the archive to have every dependency under this one directory. I also don't know why we need to increase the length of the path just to put this name in.

Copy link
Contributor Author

@ryfu-msft ryfu-msft Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a mistake with how I created the test zip. I will fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this extra directory as its not needed anymore. Basically, I compressed the folder containing the arch folders. I had to instead select all the arch folders and compress from there.

if (!File.Exists(fullPath))
{
string fullPath = 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 },
});
this.pwshCmdlet.Write(StreamType.Verbose, $"Package dependency not found in archive: {fullPath}");
return false;
}
}

return true;
}
catch (WinGetRepairException e)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Dependency assets not found in GitHub release.");
}
catch (Exception e)
{
this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to install dependencies from GitHub release. {e.ToString()}");
_ = this.ExecuteAppxCmdlet(
AddAppxPackage,
new Dictionary<string, object>
{
{ Path, fullPath },
{ ErrorAction, Stop },
});
}
}

return false;
return true;
}

private Dictionary<string, string> GetVCLibsDependencies()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="WinGetVersion.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand All @@ -7,6 +7,7 @@
namespace Microsoft.WinGet.Client.Engine.Helpers
{
using System;
using Microsoft.WinGet.Common.Command;

/// <summary>
/// WinGetVersion. Parse the string version returned by winget --version to allow comparisons.
Expand Down Expand Up @@ -48,19 +49,6 @@ public WinGetVersion(string version)
this.Version = Version.Parse(toParseVersion);
}

/// <summary>
/// Gets the version of the installed winget.
/// </summary>
public static WinGetVersion InstalledWinGetVersion
{
get
{
var wingetCliWrapper = new WingetCLIWrapper();
var result = wingetCliWrapper.RunCommand("--version");
return new WinGetVersion(result.StdOut.Replace(Environment.NewLine, string.Empty));
}
}

/// <summary>
/// Gets the version as it appears as a tag.
/// </summary>
Expand All @@ -76,6 +64,18 @@ public static WinGetVersion InstalledWinGetVersion
/// </summary>
public bool IsPrerelease { get; }

/// <summary>
/// Gets the version of the installed winget.
/// </summary>
/// <param name="pwshCmdlet">PowerShell cmdlet.</param>
/// <returns>The WinGetVersion.</returns>
public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet)
{
var wingetCliWrapper = new WingetCLIWrapper();
var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version");
return new WinGetVersion(result.StdOut.Replace(Environment.NewLine, string.Empty));
}

/// <summary>
/// Version.CompareTo taking into account prerelease.
/// From semver: Pre-release versions have a lower precedence than the associated normal version.
Expand Down
Loading
Loading