Skip to content

Commit cdd4b08

Browse files
authored
Fix Repair-WinGetPackageManager cmdlet by retrieving dependencies from GitHub assets (#4923)
1 parent b8f4133 commit cdd4b08

12 files changed

+252
-62
lines changed

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void ResetAllSources()
127127
private WinGetCLICommandResult Run(string command, string parameters, int timeOut = 60000)
128128
{
129129
var wingetCliWrapper = new WingetCLIWrapper();
130-
var result = wingetCliWrapper.RunCommand(command, parameters, timeOut);
130+
var result = wingetCliWrapper.RunCommand(this, command, parameters, timeOut);
131131
result.VerifyExitCode();
132132

133133
return result;

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// -----------------------------------------------------------------------------
1+
// -----------------------------------------------------------------------------
22
// <copyright file="UserSettingsCommand.cs" company="Microsoft Corporation">
33
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
44
// </copyright>
@@ -42,7 +42,7 @@ public UserSettingsCommand(PSCmdlet psCmdlet)
4242
if (winGetSettingsFilePath == null)
4343
{
4444
var wingetCliWrapper = new WingetCLIWrapper();
45-
var settingsResult = wingetCliWrapper.RunCommand("settings", "export");
45+
var settingsResult = wingetCliWrapper.RunCommand(this, "settings", "export");
4646

4747
// Read the user settings file property.
4848
var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile");

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/VersionCommand.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// -----------------------------------------------------------------------------
1+
// -----------------------------------------------------------------------------
22
// <copyright file="VersionCommand.cs" company="Microsoft Corporation">
33
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
44
// </copyright>
@@ -30,7 +30,7 @@ public VersionCommand(PSCmdlet psCmdlet)
3030
/// </summary>
3131
public void Get()
3232
{
33-
this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion.TagVersion);
33+
this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion(this).TagVersion);
3434
}
3535
}
3636
}

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers
169169

170170
private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, bool allUsers, bool force)
171171
{
172-
var installedVersion = WinGetVersion.InstalledWinGetVersion;
172+
var installedVersion = WinGetVersion.InstalledWinGetVersion(this);
173173
bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0;
174174

175175
string message = $"Installed WinGet version '{installedVersion.TagVersion}' " +

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// -----------------------------------------------------------------------------
1+
// -----------------------------------------------------------------------------
22
// <copyright file="WinGetIntegrity.cs" company="Microsoft Corporation">
33
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
44
// </copyright>
@@ -44,7 +44,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers
4444
// Start by calling winget without its WindowsApp PFN path.
4545
// If it succeeds and the exit code is 0 then we are good.
4646
var wingetCliWrapper = new WingetCLIWrapper(false);
47-
var result = wingetCliWrapper.RunCommand("--version");
47+
var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version");
4848
result.VerifyExitCode();
4949
}
5050
catch (Win32Exception e)
@@ -68,7 +68,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers
6868
{
6969
// This assumes caller knows that the version exist.
7070
WinGetVersion expectedWinGetVersion = new WinGetVersion(expectedVersion);
71-
var installedVersion = WinGetVersion.InstalledWinGetVersion;
71+
var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet);
7272
if (expectedWinGetVersion.CompareTo(installedVersion) != 0)
7373
{
7474
throw new WinGetIntegrityException(

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/ReleaseExtensions.cs

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// -----------------------------------------------------------------------------
1+
// -----------------------------------------------------------------------------
22
// <copyright file="ReleaseExtensions.cs" company="Microsoft Corporation">
33
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
44
// </copyright>
@@ -24,16 +24,28 @@ internal static class ReleaseExtensions
2424
/// <returns>The asset.</returns>
2525
public static ReleaseAsset GetAsset(this Release release, string name)
2626
{
27-
var assets = release.Assets.Where(a => a.Name == name);
27+
var asset = TryGetAsset(release, name);
2828

29-
if (assets.Any())
29+
if (asset != null)
3030
{
31-
return assets.First();
31+
return asset;
3232
}
3333

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

37+
/// <summary>
38+
/// Gets the Asset if present.
39+
/// </summary>
40+
/// <param name="release">GitHub release.</param>
41+
/// <param name="name">Name of asset.</param>
42+
/// <returns>The asset, or null if not found.</returns>
43+
public static ReleaseAsset? TryGetAsset(this Release release, string name)
44+
{
45+
var assets = release.Assets.Where(a => a.Name == name);
46+
return assets.Any() ? assets.First() : null;
47+
}
48+
3749
/// <summary>
3850
/// Gets the asset that ends with the string.
3951
/// </summary>

Diff for: src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs

+163-32
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ namespace Microsoft.WinGet.Client.Engine.Helpers
99
using System;
1010
using System.Collections.Generic;
1111
using System.Collections.ObjectModel;
12+
using System.IO;
13+
using System.IO.Compression;
1214
using System.Linq;
1315
using System.Management.Automation;
1416
using System.Runtime.InteropServices;
1517
using System.Threading.Tasks;
1618
using Microsoft.WinGet.Client.Engine.Common;
19+
using Microsoft.WinGet.Client.Engine.Exceptions;
1720
using Microsoft.WinGet.Client.Engine.Extensions;
1821
using Microsoft.WinGet.Common.Command;
22+
using Newtonsoft.Json;
1923
using Octokit;
2024
using Semver;
2125
using static Microsoft.WinGet.Client.Engine.Common.Constants;
@@ -63,8 +67,13 @@ internal class AppxModuleHelper
6367

6468
// Assets
6569
private const string MsixBundleName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle";
70+
private const string DependenciesJsonName = "DesktopAppInstaller_Dependencies.json";
71+
private const string DependenciesZipName = "DesktopAppInstaller_Dependencies.zip";
6672
private const string License = "License1.xml";
6773

74+
// Format of a dependency package such as 'x64\Microsoft.VCLibs.140.00.UWPDesktop_14.0.33728.0_x64.appx'
75+
private const string ExtractedDependencyPath = "{0}\\{1}_{2}_{0}.appx";
76+
6877
// Dependencies
6978
// VCLibs
7079
private const string VCLibsUWPDesktop = "Microsoft.VCLibs.140.00.UWPDesktop";
@@ -319,53 +328,67 @@ private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade,
319328

320329
private async Task InstallDependenciesAsync(string releaseTag)
321330
{
322-
// A better implementation would use Add-AppxPackage with -DependencyPath, but
323-
// the Appx module needs to be remoted into Windows PowerShell. When the string[] parameter
324-
// gets deserialized from Core the result is a single string which breaks Add-AppxPackage.
325-
// Here we should: if we are in Windows Powershell then run Add-AppxPackage with -DependencyPath
326-
// if we are in Core, then start powershell.exe and run the same command. Right now, we just
327-
// do Add-AppxPackage for each one.
328-
await this.InstallVCLibsDependenciesAsync();
329-
await this.InstallUiXamlAsync(releaseTag);
331+
bool result = await this.InstallDependenciesFromGitHubArchive(releaseTag);
332+
333+
if (!result)
334+
{
335+
// A better implementation would use Add-AppxPackage with -DependencyPath, but
336+
// the Appx module needs to be remoted into Windows PowerShell. When the string[] parameter
337+
// gets deserialized from Core the result is a single string which breaks Add-AppxPackage.
338+
// Here we should: if we are in Windows Powershell then run Add-AppxPackage with -DependencyPath
339+
// if we are in Core, then start powershell.exe and run the same command. Right now, we just
340+
// do Add-AppxPackage for each one.
341+
// This method no longer works for versions >1.9 as the vclibs url has been deprecated.
342+
await this.InstallVCLibsDependenciesFromUriAsync();
343+
await this.InstallUiXamlAsync(releaseTag);
344+
}
330345
}
331346

332-
private async Task InstallVCLibsDependenciesAsync()
347+
private Dictionary<string, string> GetDependenciesByArch(PackageDependency dependencies)
333348
{
334-
var result = this.ExecuteAppxCmdlet(
335-
GetAppxPackage,
336-
new Dictionary<string, object>
337-
{
338-
{ Name, VCLibsUWPDesktop },
339-
});
349+
Dictionary<string, string> appxPackages = new Dictionary<string, string>();
350+
var arch = RuntimeInformation.OSArchitecture;
340351

341-
// See if the minimum (or greater) version is installed.
342-
// TODO: Pull the minimum version from the target package
343-
Version minimumVersion = new Version(VCLibsUWPDesktopVersion);
352+
string appxPackageX64 = string.Format(ExtractedDependencyPath, "x64", dependencies.Name, dependencies.Version);
353+
string appxPackageX86 = string.Format(ExtractedDependencyPath, "x86", dependencies.Name, dependencies.Version);
354+
string appxPackageArm = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version);
355+
string appxPackageArm64 = string.Format(ExtractedDependencyPath, "arm", dependencies.Name, dependencies.Version);
344356

345-
// Construct the list of frameworks that we want present.
346-
Dictionary<string, string> vcLibsDependencies = new Dictionary<string, string>();
347-
var arch = RuntimeInformation.OSArchitecture;
348357
if (arch == Architecture.X64)
349358
{
350-
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
359+
appxPackages.Add("x64", appxPackageX64);
351360
}
352361
else if (arch == Architecture.X86)
353362
{
354-
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
363+
appxPackages.Add("x86", appxPackageX86);
355364
}
356365
else if (arch == Architecture.Arm64)
357366
{
358367
// Deployment please figure out for me.
359-
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
360-
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
361-
vcLibsDependencies.Add("arm", VCLibsUWPDesktopArm);
362-
vcLibsDependencies.Add("arm64", VCLibsUWPDesktopArm64);
368+
appxPackages.Add("x64", appxPackageX64);
369+
appxPackages.Add("x86", appxPackageX86);
370+
appxPackages.Add("arm", appxPackageArm);
371+
appxPackages.Add("arm64", appxPackageArm64);
363372
}
364373
else
365374
{
366375
throw new PSNotSupportedException(arch.ToString());
367376
}
368377

378+
return appxPackages;
379+
}
380+
381+
private void FindMissingDependencies(Dictionary<string, string> dependencies, string packageName, string requiredVersion)
382+
{
383+
var result = this.ExecuteAppxCmdlet(
384+
GetAppxPackage,
385+
new Dictionary<string, object>
386+
{
387+
{ Name, packageName },
388+
});
389+
390+
Version minimumVersion = new Version(requiredVersion);
391+
369392
if (result != null &&
370393
result.Count > 0)
371394
{
@@ -384,24 +407,30 @@ private async Task InstallVCLibsDependenciesAsync()
384407
string? architectureString = psobject?.Architecture?.ToString();
385408
if (architectureString == null)
386409
{
387-
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs dependency has no architecture value: {psobject?.PackageFullName ?? "<null>"}");
410+
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} dependency has no architecture value: {psobject?.PackageFullName ?? "<null>"}");
388411
continue;
389412
}
390413

391414
architectureString = architectureString.ToLower();
392415

393-
if (vcLibsDependencies.ContainsKey(architectureString))
416+
if (dependencies.ContainsKey(architectureString))
394417
{
395-
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs {architectureString} dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}");
396-
vcLibsDependencies.Remove(architectureString);
418+
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} {architectureString} dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}");
419+
dependencies.Remove(architectureString);
397420
}
398421
}
399422
else
400423
{
401-
this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs is lower than minimum required version [{minimumVersion}]: {psobject?.PackageFullName ?? "<null>"}");
424+
this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} is lower than minimum required version [{minimumVersion}]: {psobject?.PackageFullName ?? "<null>"}");
402425
}
403426
}
404427
}
428+
}
429+
430+
private async Task InstallVCLibsDependenciesFromUriAsync()
431+
{
432+
Dictionary<string, string> vcLibsDependencies = this.GetVCLibsDependencies();
433+
this.FindMissingDependencies(vcLibsDependencies, VCLibsUWPDesktop, VCLibsUWPDesktopVersion);
405434

406435
if (vcLibsDependencies.Count != 0)
407436
{
@@ -419,6 +448,108 @@ private async Task InstallVCLibsDependenciesAsync()
419448
}
420449
}
421450

451+
// Returns a boolean value indicating whether dependencies were successfully installed from the GitHub release assets.
452+
private async Task<bool> InstallDependenciesFromGitHubArchive(string releaseTag)
453+
{
454+
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
455+
var release = await githubClient.GetReleaseAsync(releaseTag);
456+
457+
ReleaseAsset? dependenciesJsonAsset = release.TryGetAsset(DependenciesJsonName);
458+
if (dependenciesJsonAsset is null)
459+
{
460+
return false;
461+
}
462+
463+
using var dependenciesJsonFile = new TempFile();
464+
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesJsonAsset.BrowserDownloadUrl, dependenciesJsonFile.FullPath, this.pwshCmdlet);
465+
466+
using StreamReader r = new StreamReader(dependenciesJsonFile.FullPath);
467+
string json = r.ReadToEnd();
468+
WingetDependencies? wingetDependencies = JsonConvert.DeserializeObject<WingetDependencies>(json);
469+
470+
if (wingetDependencies is null)
471+
{
472+
this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to deserialize dependencies json file.");
473+
return false;
474+
}
475+
476+
List<string> missingDependencies = new List<string>();
477+
foreach (var dependency in wingetDependencies.Dependencies)
478+
{
479+
Dictionary<string, string> dependenciesByArch = this.GetDependenciesByArch(dependency);
480+
this.FindMissingDependencies(dependenciesByArch, dependency.Name, dependency.Version);
481+
482+
foreach (var pair in dependenciesByArch)
483+
{
484+
missingDependencies.Add(pair.Value);
485+
}
486+
}
487+
488+
if (missingDependencies.Count != 0)
489+
{
490+
using var dependenciesZipFile = new TempFile();
491+
using var extractedDirectory = new TempDirectory();
492+
493+
ReleaseAsset? dependenciesZipAsset = release.TryGetAsset(DependenciesZipName);
494+
if (dependenciesZipAsset is null)
495+
{
496+
this.pwshCmdlet.Write(StreamType.Verbose, $"Dependencies zip asset not found on GitHub asset.");
497+
return false;
498+
}
499+
500+
await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesZipAsset.BrowserDownloadUrl, dependenciesZipFile.FullPath, this.pwshCmdlet);
501+
ZipFile.ExtractToDirectory(dependenciesZipFile.FullPath, extractedDirectory.FullDirectoryPath);
502+
503+
foreach (var entry in missingDependencies)
504+
{
505+
string fullPath = System.IO.Path.Combine(extractedDirectory.FullDirectoryPath, entry);
506+
if (!File.Exists(fullPath))
507+
{
508+
this.pwshCmdlet.Write(StreamType.Verbose, $"Package dependency not found in archive: {fullPath}");
509+
return false;
510+
}
511+
512+
_ = this.ExecuteAppxCmdlet(
513+
AddAppxPackage,
514+
new Dictionary<string, object>
515+
{
516+
{ Path, fullPath },
517+
{ ErrorAction, Stop },
518+
});
519+
}
520+
}
521+
522+
return true;
523+
}
524+
525+
private Dictionary<string, string> GetVCLibsDependencies()
526+
{
527+
Dictionary<string, string> vcLibsDependencies = new Dictionary<string, string>();
528+
var arch = RuntimeInformation.OSArchitecture;
529+
if (arch == Architecture.X64)
530+
{
531+
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
532+
}
533+
else if (arch == Architecture.X86)
534+
{
535+
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
536+
}
537+
else if (arch == Architecture.Arm64)
538+
{
539+
// Deployment please figure out for me.
540+
vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64);
541+
vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86);
542+
vcLibsDependencies.Add("arm", VCLibsUWPDesktopArm);
543+
vcLibsDependencies.Add("arm64", VCLibsUWPDesktopArm64);
544+
}
545+
else
546+
{
547+
throw new PSNotSupportedException(arch.ToString());
548+
}
549+
550+
return vcLibsDependencies;
551+
}
552+
422553
private async Task InstallUiXamlAsync(string releaseTag)
423554
{
424555
(string xamlPackageName, string xamlReleaseTag) = GetXamlDependencyVersionInfo(releaseTag);

0 commit comments

Comments
 (0)