AppxModuleHelper.cs (35555B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="AppxModuleHelper.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace Microsoft.WinGet.Client.Engine.Helpers 8 { 9 using System; 10 using System.Collections.Generic; 11 using System.Collections.ObjectModel; 12 using System.IO; 13 using System.IO.Compression; 14 using System.Linq; 15 using System.Management.Automation; 16 using System.Runtime.InteropServices; 17 using System.Threading.Tasks; 18 using Microsoft.WinGet.Client.Engine.Common; 19 using Microsoft.WinGet.Client.Engine.Exceptions; 20 using Microsoft.WinGet.Client.Engine.Extensions; 21 using Microsoft.WinGet.Common.Command; 22 using Newtonsoft.Json; 23 using Octokit; 24 using Semver; 25 using static Microsoft.WinGet.Client.Engine.Common.Constants; 26 27 /// <summary> 28 /// Helper to make calls to the Appx module. 29 /// </summary> 30 internal class AppxModuleHelper 31 { 32 // Cmdlets 33 private const string ImportModule = "Import-Module"; 34 private const string GetAppxPackage = "Get-AppxPackage"; 35 private const string AddAppxPackage = "Add-AppxPackage"; 36 private const string AddAppxProvisionedPackage = "Add-AppxProvisionedPackage"; 37 private const string GetCommand = "Get-Command"; 38 39 // Parameters name 40 private const string Name = "Name"; 41 private const string Path = "Path"; 42 private const string ErrorAction = "ErrorAction"; 43 private const string WarningAction = "WarningAction"; 44 private const string PackagePath = "PackagePath"; 45 private const string LicensePath = "LicensePath"; 46 private const string Module = "Module"; 47 private const string StubPackageOption = "StubPackageOption"; 48 private const string PackageTypeFilter = "PackageTypeFilter"; 49 50 // Parameter Values 51 private const string Appx = "Appx"; 52 private const string Stop = "Stop"; 53 private const string SilentlyContinue = "SilentlyContinue"; 54 private const string Online = "Online"; 55 private const string UsePreference = "UsePreference"; 56 private const string Framework = "Framework"; 57 58 // Options 59 private const string UseWindowsPowerShell = "UseWindowsPowerShell"; 60 private const string ForceUpdateFromAnyVersion = "ForceUpdateFromAnyVersion"; 61 private const string Register = "Register"; 62 private const string DisableDevelopmentMode = "DisableDevelopmentMode"; 63 private const string ForceTargetApplicationShutdown = "ForceTargetApplicationShutdown"; 64 65 private const string AppInstallerName = "Microsoft.DesktopAppInstaller"; 66 private const string AppxManifest = "AppxManifest.xml"; 67 private const string PackageFullName = "PackageFullName"; 68 private const string Version = "Version"; 69 70 private const string DependencyArchitectureEnvironmentVariable = "WINGET_PACKAGE_MANAGER_REPAIR_DEPENDENCY_ARCHITECTURES"; 71 72 // Assets 73 private const string MsixBundleName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"; 74 private const string DependenciesJsonName = "DesktopAppInstaller_Dependencies.json"; 75 private const string DependenciesZipName = "DesktopAppInstaller_Dependencies.zip"; 76 private const string License = "License1.xml"; 77 78 // Format of a dependency package such as 'x64\Microsoft.VCLibs.140.00.UWPDesktop_14.0.33728.0_x64.appx' 79 private const string ExtractedDependencyPath = "{0}\\{1}_{2}_{0}.appx"; 80 81 // Dependencies 82 // VCLibs 83 private const string VCLibsUWPDesktop = "Microsoft.VCLibs.140.00.UWPDesktop"; 84 private const string VCLibsUWPDesktopVersion = "14.0.30704.0"; 85 private const string VCLibsUWPDesktopX64 = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx"; 86 private const string VCLibsUWPDesktopX86 = "https://aka.ms/Microsoft.VCLibs.x86.14.00.Desktop.appx"; 87 private const string VCLibsUWPDesktopArm64 = "https://aka.ms/Microsoft.VCLibs.arm64.14.00.Desktop.appx"; 88 89 // Xaml 90 private const string XamlPackage28 = "Microsoft.UI.Xaml.2.8"; 91 private const string XamlReleaseTag286 = "v2.8.6"; 92 private const string MinimumWinGetReleaseTagForXaml28 = "v1.7.10514"; 93 94 private const string XamlPackage27 = "Microsoft.UI.Xaml.2.7"; 95 private const string XamlReleaseTag273 = "v2.7.3"; 96 97 private readonly PowerShellCmdlet pwshCmdlet; 98 private readonly HttpClientHelper httpClientHelper; 99 private Lazy<HashSet<Architecture>> frameworkArchitectures; 100 101 /// <summary> 102 /// Initializes a new instance of the <see cref="AppxModuleHelper"/> class. 103 /// </summary> 104 /// <param name="pwshCmdlet">The calling cmdlet.</param> 105 public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) 106 { 107 this.pwshCmdlet = pwshCmdlet; 108 this.httpClientHelper = new HttpClientHelper(); 109 this.frameworkArchitectures = new Lazy<HashSet<Architecture>>(() => this.InitFrameworkArchitectures()); 110 } 111 112 /// <summary> 113 /// Calls Get-AppxPackage Microsoft.DesktopAppInstaller. 114 /// </summary> 115 /// <returns>Result of Get-AppxPackage.</returns> 116 public PSObject? GetAppInstallerObject() 117 { 118 return this.GetAppxObject(AppInstallerName); 119 } 120 121 /// <summary> 122 /// Gets the string value a property from the Get-AppxPackage object of AppInstaller. 123 /// </summary> 124 /// <param name="propertyName">Property name.</param> 125 /// <returns>Value, null if doesn't exist.</returns> 126 public string? GetAppInstallerPropertyValue(string propertyName) 127 { 128 string? result = null; 129 var packageObj = this.GetAppInstallerObject(); 130 if (packageObj is not null) 131 { 132 var property = packageObj.Properties.Where(p => p.Name == propertyName).FirstOrDefault(); 133 if (property is not null) 134 { 135 result = property.Value as string; 136 } 137 } 138 139 return result; 140 } 141 142 /// <summary> 143 /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml. 144 /// </summary> 145 /// <param name="releaseTag">Release tag of GitHub release.</param> 146 public void RegisterAppInstaller(string releaseTag) 147 { 148 if (string.IsNullOrEmpty(releaseTag)) 149 { 150 string? versionFromLocalPackage = this.GetAppInstallerPropertyValue(Version); 151 152 if (versionFromLocalPackage == null) 153 { 154 throw new ArgumentNullException(Version); 155 } 156 157 var packageVersion = new Version(versionFromLocalPackage); 158 if (packageVersion.Major == 1 && packageVersion.Minor > 15) 159 { 160 releaseTag = $"1.{packageVersion.Minor - 15}.{packageVersion.Build}"; 161 } 162 else 163 { 164 releaseTag = $"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}"; 165 } 166 } 167 168 // Ensure that all dependencies are present when attempting to register. 169 // If dependencies are missing, a provisioned package can appear to only need registration, 170 // but will fail to register. `InstallDependenciesAsync` checks for the packages before 171 // acting, so it should be mostly a no-op if they are already available. 172 this.InstallDependenciesAsync(releaseTag).Wait(); 173 174 string? packageFullName = this.GetAppInstallerPropertyValue(PackageFullName); 175 176 if (packageFullName == null) 177 { 178 throw new ArgumentNullException(PackageFullName); 179 } 180 181 string appxManifestPath = System.IO.Path.Combine( 182 Utilities.ProgramFilesWindowsAppPath, 183 packageFullName, 184 AppxManifest); 185 186 _ = this.ExecuteAppxCmdlet( 187 AddAppxPackage, 188 new Dictionary<string, object> 189 { 190 { Path, appxManifestPath }, 191 }, 192 new List<string> 193 { 194 Register, 195 DisableDevelopmentMode, 196 }); 197 } 198 199 /// <summary> 200 /// Install AppInstaller's bundle from a GitHub release. 201 /// </summary> 202 /// <param name="releaseTag">Release tag of GitHub release.</param> 203 /// <param name="allUsers">If install for all users is needed.</param> 204 /// <param name="isDowngrade">Is downgrade.</param> 205 /// <param name="force">Force application shutdown.</param> 206 /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> 207 public async Task InstallFromGitHubReleaseAsync(string releaseTag, bool allUsers, bool isDowngrade, bool force) 208 { 209 await this.InstallDependenciesAsync(releaseTag); 210 211 if (isDowngrade) 212 { 213 // Add-AppxProvisionedPackage doesn't support downgrade. 214 await this.AddAppInstallerBundleAsync(releaseTag, true, force); 215 216 if (allUsers) 217 { 218 await this.AddProvisionPackageAsync(releaseTag); 219 } 220 } 221 else 222 { 223 if (allUsers) 224 { 225 await this.AddProvisionPackageAsync(releaseTag); 226 } 227 else 228 { 229 await this.AddAppInstallerBundleAsync(releaseTag, false, force); 230 } 231 } 232 } 233 234 /// <summary> 235 /// Gets the Xaml dependency package name and release tag based on the provided WinGet release tag. 236 /// </summary> 237 /// <param name="releaseTag">WinGet release tag.</param> 238 /// <returns>A tuple in the format of (XamlPackageName, XamlReleaseTag).</returns> 239 private static Tuple<string, string> GetXamlDependencyVersionInfo(string releaseTag) 240 { 241 var targetVersion = SemVersion.Parse(releaseTag, SemVersionStyles.AllowLowerV); 242 243 if (targetVersion.CompareSortOrderTo(SemVersion.Parse(MinimumWinGetReleaseTagForXaml28, SemVersionStyles.AllowLowerV)) >= 0) 244 { 245 return Tuple.Create(XamlPackage28, XamlReleaseTag286); 246 } 247 else 248 { 249 return Tuple.Create(XamlPackage27, XamlReleaseTag273); 250 } 251 } 252 253 private async Task AddProvisionPackageAsync(string releaseTag) 254 { 255 var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); 256 var release = await githubClient.GetReleaseAsync(releaseTag); 257 258 var bundleAsset = release.GetAsset(MsixBundleName); 259 using var bundleFile = new TempFile(fileName: MsixBundleName); 260 await this.httpClientHelper.DownloadUrlWithProgressAsync( 261 bundleAsset.BrowserDownloadUrl, bundleFile.FullPath, this.pwshCmdlet); 262 263 var licenseAsset = release.GetAssetEndsWith(License); 264 using var licenseFile = new TempFile(fileName: licenseAsset.Name); 265 await this.httpClientHelper.DownloadUrlWithProgressAsync( 266 licenseAsset.BrowserDownloadUrl, licenseFile.FullPath, this.pwshCmdlet); 267 268 try 269 { 270 this.pwshCmdlet.ExecuteInPowerShellThread( 271 () => 272 { 273 var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); 274 ps.AddCommand(AddAppxProvisionedPackage) 275 .AddParameter(Online) 276 .AddParameter(PackagePath, bundleFile.FullPath) 277 .AddParameter(LicensePath, licenseFile.FullPath) 278 .AddParameter(ErrorAction, Stop) 279 .Invoke(); 280 }); 281 } 282 catch (RuntimeException e) 283 { 284 this.pwshCmdlet.Write(StreamType.Verbose, $"Failed installing bundle via Add-AppxProvisionedPackage {e}"); 285 throw; 286 } 287 } 288 289 private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade, bool force) 290 { 291 var options = new List<string>(); 292 if (downgrade) 293 { 294 options.Add(ForceUpdateFromAnyVersion); 295 } 296 297 if (force) 298 { 299 options.Add(ForceTargetApplicationShutdown); 300 } 301 302 var parameters = new Dictionary<string, object>(); 303 if (this.IsStubPackageOptionPresent()) 304 { 305 parameters.Add(StubPackageOption, UsePreference); 306 } 307 308 try 309 { 310 var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); 311 var release = await githubClient.GetReleaseAsync(releaseTag); 312 313 var bundleAsset = release.GetAsset(MsixBundleName); 314 await this.AddAppxPackageAsUriAsync(bundleAsset.BrowserDownloadUrl, MsixBundleName, parameters, options); 315 } 316 catch (RuntimeException e) 317 { 318 this.pwshCmdlet.Write(StreamType.Verbose, $"Failed installing bundle via Add-AppxPackage {e}"); 319 throw; 320 } 321 } 322 323 private PSObject? GetAppxObject(string packageName) 324 { 325 return this.ExecuteAppxCmdlet( 326 GetAppxPackage, 327 new Dictionary<string, object> 328 { 329 { Name, packageName }, 330 }) 331 .FirstOrDefault(); 332 } 333 334 private async Task InstallDependenciesAsync(string releaseTag) 335 { 336 bool result = await this.InstallDependenciesFromGitHubArchive(releaseTag); 337 338 if (!result) 339 { 340 // A better implementation would use Add-AppxPackage with -DependencyPath, but 341 // the Appx module needs to be remoted into Windows PowerShell. When the string[] parameter 342 // gets deserialized from Core the result is a single string which breaks Add-AppxPackage. 343 // Here we should: if we are in Windows Powershell then run Add-AppxPackage with -DependencyPath 344 // if we are in Core, then start powershell.exe and run the same command. Right now, we just 345 // do Add-AppxPackage for each one. 346 // This method no longer works for versions >1.9 as the vclibs url has been deprecated. 347 await this.InstallVCLibsDependenciesFromUriAsync(); 348 await this.InstallUiXamlAsync(releaseTag); 349 } 350 } 351 352 /// <summary> 353 /// Extracts all of the architectures used by framework packages. 354 /// </summary> 355 /// <returns>The set of architectures used by installed framework packages.</returns> 356 private HashSet<Architecture> InitFrameworkArchitectures() 357 { 358 HashSet<Architecture> architectures = new HashSet<Architecture>(); 359 360 // Read the override from the environment variable if it exists. 361 string? environmentVariable = Environment.GetEnvironmentVariable(DependencyArchitectureEnvironmentVariable); 362 if (environmentVariable != null) 363 { 364 this.pwshCmdlet.Write(StreamType.Verbose, $"Using environment variable {DependencyArchitectureEnvironmentVariable} for frameworks: {environmentVariable}"); 365 366 foreach (string architectureString in environmentVariable.Split(',', ';')) 367 { 368 Architecture architecture; 369 if (Enum.TryParse(architectureString, true, out architecture)) 370 { 371 if (architectures.Add(architecture)) 372 { 373 this.pwshCmdlet.Write(StreamType.Verbose, $"Framework architecture from environment variable: {architectureString}"); 374 } 375 } 376 } 377 378 return architectures; 379 } 380 381 // If there are any framework packages already installed, use the same architecture as them. 382 var result = this.ExecuteAppxCmdlet( 383 GetAppxPackage, 384 new Dictionary<string, object> 385 { 386 { PackageTypeFilter, Framework }, 387 }); 388 389 if (result != null && 390 result.Count > 0) 391 { 392 foreach (dynamic psobject in result) 393 { 394 string? architectureString = psobject?.Architecture?.ToString(); 395 if (architectureString == null) 396 { 397 continue; 398 } 399 400 Architecture architecture; 401 if (Enum.TryParse(architectureString, true, out architecture)) 402 { 403 if (architectures.Add(architecture)) 404 { 405 this.pwshCmdlet.Write(StreamType.Verbose, $"Found framework architecture: {architectureString}"); 406 } 407 } 408 } 409 } 410 411 // Fall back to guessing from the current OS architecture. 412 // This may have issues on ARM64 because RuntimeInformation.OSArchitecture seems to just lie sometimes. 413 // See https://github.com/microsoft/winget-cli/issues/5020 414 if (architectures.Count == 0) 415 { 416 var arch = RuntimeInformation.OSArchitecture; 417 this.pwshCmdlet.Write(StreamType.Verbose, $"OS architecture: {arch.ToString()}"); 418 419 if (arch == Architecture.X64) 420 { 421 architectures.Add(Architecture.X64); 422 } 423 else if (arch == Architecture.X86) 424 { 425 architectures.Add(Architecture.X86); 426 } 427 else if (arch == Architecture.Arm64) 428 { 429 // Let deployment figure it out 430 architectures.Add(Architecture.Arm64); 431 architectures.Add(Architecture.X64); 432 architectures.Add(Architecture.X86); 433 } 434 } 435 436 return architectures; 437 } 438 439 private Dictionary<string, string> GetDependenciesByArch(PackageDependency dependencies) 440 { 441 Dictionary<string, string> appxPackages = new Dictionary<string, string>(); 442 443 foreach (var architecture in this.frameworkArchitectures.Value) 444 { 445 switch (architecture) 446 { 447 case Architecture.X86: 448 appxPackages.Add("x86", string.Format(ExtractedDependencyPath, "x86", dependencies.Name, dependencies.Version)); 449 break; 450 case Architecture.X64: 451 appxPackages.Add("x64", string.Format(ExtractedDependencyPath, "x64", dependencies.Name, dependencies.Version)); 452 break; 453 case Architecture.Arm64: 454 appxPackages.Add("arm64", string.Format(ExtractedDependencyPath, "arm64", dependencies.Name, dependencies.Version)); 455 break; 456 default: 457 this.pwshCmdlet.Write(StreamType.Verbose, $"GetDependenciesByArch: Ignoring {architecture}"); 458 break; 459 } 460 } 461 462 return appxPackages; 463 } 464 465 private void FindMissingDependencies(Dictionary<string, string> dependencies, string packageName, string requiredVersion) 466 { 467 var result = this.ExecuteAppxCmdlet( 468 GetAppxPackage, 469 new Dictionary<string, object> 470 { 471 { Name, packageName }, 472 }); 473 474 Version minimumVersion = new Version(requiredVersion); 475 476 if (result != null && 477 result.Count > 0) 478 { 479 foreach (dynamic psobject in result) 480 { 481 string? versionString = psobject?.Version?.ToString(); 482 if (versionString == null) 483 { 484 continue; 485 } 486 487 Version packageVersion = new Version(versionString); 488 489 if (packageVersion >= minimumVersion) 490 { 491 string? architectureString = psobject?.Architecture?.ToString(); 492 if (architectureString == null) 493 { 494 this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} dependency has no architecture value: {psobject?.PackageFullName ?? "<null>"}"); 495 continue; 496 } 497 498 architectureString = architectureString.ToLower(); 499 500 if (dependencies.ContainsKey(architectureString)) 501 { 502 this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} {architectureString} dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}"); 503 dependencies.Remove(architectureString); 504 } 505 } 506 else 507 { 508 this.pwshCmdlet.Write(StreamType.Verbose, $"{packageName} is lower than minimum required version [{minimumVersion}]: {psobject?.PackageFullName ?? "<null>"}"); 509 } 510 } 511 } 512 } 513 514 private async Task InstallVCLibsDependenciesFromUriAsync() 515 { 516 Dictionary<string, string> vcLibsDependencies = this.GetVCLibsDependencies(); 517 this.FindMissingDependencies(vcLibsDependencies, VCLibsUWPDesktop, VCLibsUWPDesktopVersion); 518 519 if (vcLibsDependencies.Count != 0) 520 { 521 this.pwshCmdlet.Write(StreamType.Verbose, "Couldn't find required VCLibs packages"); 522 523 foreach (var vclibPair in vcLibsDependencies) 524 { 525 string vclib = vclibPair.Value; 526 await this.AddAppxPackageAsUriAsync(vclib, vclib.Substring(vclib.LastIndexOf('/') + 1)); 527 } 528 } 529 else 530 { 531 this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs are updated."); 532 } 533 } 534 535 // Returns a boolean value indicating whether dependencies were successfully installed from the GitHub release assets. 536 private async Task<bool> InstallDependenciesFromGitHubArchive(string releaseTag) 537 { 538 var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); 539 var release = await githubClient.GetReleaseAsync(releaseTag); 540 541 ReleaseAsset? dependenciesJsonAsset = release.TryGetAsset(DependenciesJsonName); 542 if (dependenciesJsonAsset is null) 543 { 544 return false; 545 } 546 547 using var dependenciesJsonFile = new TempFile(); 548 await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesJsonAsset.BrowserDownloadUrl, dependenciesJsonFile.FullPath, this.pwshCmdlet); 549 550 using StreamReader r = new StreamReader(dependenciesJsonFile.FullPath); 551 string json = r.ReadToEnd(); 552 WingetDependencies? wingetDependencies = JsonConvert.DeserializeObject<WingetDependencies>(json); 553 554 if (wingetDependencies is null) 555 { 556 this.pwshCmdlet.Write(StreamType.Verbose, $"Failed to deserialize dependencies json file."); 557 return false; 558 } 559 560 List<string> missingDependencies = new List<string>(); 561 foreach (var dependency in wingetDependencies.Dependencies) 562 { 563 Dictionary<string, string> dependenciesByArch = this.GetDependenciesByArch(dependency); 564 this.FindMissingDependencies(dependenciesByArch, dependency.Name, dependency.Version); 565 566 foreach (var pair in dependenciesByArch) 567 { 568 missingDependencies.Add(pair.Value); 569 } 570 } 571 572 if (missingDependencies.Count != 0) 573 { 574 using var dependenciesZipFile = new TempFile(); 575 using var extractedDirectory = new TempDirectory(); 576 577 ReleaseAsset? dependenciesZipAsset = release.TryGetAsset(DependenciesZipName); 578 if (dependenciesZipAsset is null) 579 { 580 this.pwshCmdlet.Write(StreamType.Verbose, $"Dependencies zip asset not found on GitHub asset."); 581 return false; 582 } 583 584 await this.httpClientHelper.DownloadUrlWithProgressAsync(dependenciesZipAsset.BrowserDownloadUrl, dependenciesZipFile.FullPath, this.pwshCmdlet); 585 ZipFile.ExtractToDirectory(dependenciesZipFile.FullPath, extractedDirectory.FullDirectoryPath); 586 587 foreach (var entry in missingDependencies) 588 { 589 string fullPath = System.IO.Path.Combine(extractedDirectory.FullDirectoryPath, entry); 590 if (!File.Exists(fullPath)) 591 { 592 this.pwshCmdlet.Write(StreamType.Verbose, $"Package dependency not found in archive: {fullPath}"); 593 return false; 594 } 595 596 _ = this.ExecuteAppxCmdlet( 597 AddAppxPackage, 598 new Dictionary<string, object> 599 { 600 { Path, fullPath }, 601 { ErrorAction, Stop }, 602 }); 603 } 604 } 605 606 return true; 607 } 608 609 private Dictionary<string, string> GetVCLibsDependencies() 610 { 611 Dictionary<string, string> vcLibsDependencies = new Dictionary<string, string>(); 612 613 foreach (var architecture in this.frameworkArchitectures.Value) 614 { 615 switch (architecture) 616 { 617 case Architecture.X86: 618 vcLibsDependencies.Add("x86", VCLibsUWPDesktopX86); 619 break; 620 case Architecture.X64: 621 vcLibsDependencies.Add("x64", VCLibsUWPDesktopX64); 622 break; 623 case Architecture.Arm64: 624 vcLibsDependencies.Add("arm64", VCLibsUWPDesktopArm64); 625 break; 626 default: 627 this.pwshCmdlet.Write(StreamType.Verbose, $"GetVCLibsDependencies: Ignoring {architecture}"); 628 break; 629 } 630 } 631 632 return vcLibsDependencies; 633 } 634 635 private async Task InstallUiXamlAsync(string releaseTag) 636 { 637 (string xamlPackageName, string xamlReleaseTag) = GetXamlDependencyVersionInfo(releaseTag); 638 string xamlAssetX64 = string.Format("{0}.x64.appx", xamlPackageName); 639 string xamlAssetX86 = string.Format("{0}.x86.appx", xamlPackageName); 640 string xamlAssetArm64 = string.Format("{0}.arm64.appx", xamlPackageName); 641 642 var uiXamlObjs = this.GetAppxObject(xamlPackageName); 643 if (uiXamlObjs is null) 644 { 645 var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.UiXaml); 646 647 var xamlRelease = await githubRelease.GetReleaseAsync(xamlReleaseTag); 648 649 var packagesToInstall = new List<ReleaseAsset>(); 650 651 foreach (var architecture in this.frameworkArchitectures.Value) 652 { 653 switch (architecture) 654 { 655 case Architecture.X86: 656 packagesToInstall.Add(xamlRelease.GetAsset(xamlAssetX86)); 657 break; 658 case Architecture.X64: 659 packagesToInstall.Add(xamlRelease.GetAsset(xamlAssetX64)); 660 break; 661 case Architecture.Arm64: 662 packagesToInstall.Add(xamlRelease.GetAsset(xamlAssetArm64)); 663 break; 664 default: 665 this.pwshCmdlet.Write(StreamType.Verbose, $"InstallUiXamlAsync: Ignoring {architecture}"); 666 break; 667 } 668 } 669 670 foreach (var package in packagesToInstall) 671 { 672 await this.AddAppxPackageAsUriAsync(package.BrowserDownloadUrl, package.Name); 673 } 674 } 675 } 676 677 private async Task AddAppxPackageAsUriAsync(string packageUri, string fileName, Dictionary<string, object>? parameters = null, IList<string>? options = null) 678 { 679 try 680 { 681 var thisParams = new Dictionary<string, object> 682 { 683 { Path, packageUri }, 684 { ErrorAction, Stop }, 685 }; 686 687 if (parameters != null) 688 { 689 foreach (var param in parameters) 690 { 691 thisParams.Add(param.Key, param.Value); 692 } 693 } 694 695 _ = this.ExecuteAppxCmdlet( 696 AddAppxPackage, 697 thisParams, 698 options); 699 } 700 catch (RuntimeException e) 701 { 702 // If we couldn't install it via URI, try download and install. 703 if (e.ErrorRecord.CategoryInfo.Category == ErrorCategory.OpenError) 704 { 705 this.pwshCmdlet.Write(StreamType.Verbose, $"Failed adding package [{packageUri}]. Retrying downloading it."); 706 await this.DownloadPackageAndAddAsync(packageUri, fileName, options); 707 } 708 else 709 { 710 this.pwshCmdlet.Write(StreamType.Error, e.ErrorRecord); 711 throw; 712 } 713 } 714 } 715 716 private async Task DownloadPackageAndAddAsync(string packageUrl, string fileName, IList<string>? options) 717 { 718 using var tempFile = new TempFile(fileName: fileName); 719 720 await this.httpClientHelper.DownloadUrlWithProgressAsync(packageUrl, tempFile.FullPath, this.pwshCmdlet); 721 722 _ = this.ExecuteAppxCmdlet( 723 AddAppxPackage, 724 new Dictionary<string, object> 725 { 726 { Path, tempFile.FullPath }, 727 { ErrorAction, Stop }, 728 }, 729 options); 730 } 731 732 private Collection<PSObject> ExecuteAppxCmdlet(string cmdlet, Dictionary<string, object>? parameters = null, IList<string>? options = null) 733 { 734 Collection<PSObject> result = new Collection<PSObject>(); 735 736 this.pwshCmdlet.ExecuteInPowerShellThread( 737 () => 738 { 739 var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); 740 741 // There's a bug in the Appx Module that it can't be loaded from Core in pre 10.0.22453.0 builds without 742 // the -UseWindowsPowerShell option. In post 10.0.22453.0 builds there's really no difference between 743 // using or not -UseWindowsPowerShell as it will automatically get loaded using WinPSCompatSession remoting session. 744 // https://github.com/PowerShell/PowerShell/issues/13138. 745 // Set warning action to silently continue to avoid the console with 746 // 'Module Appx is loaded in Windows PowerShell using WinPSCompatSession remoting session' 747 #if !POWERSHELL_WINDOWS 748 ps.AddCommand(ImportModule) 749 .AddParameter(Name, Appx) 750 .AddParameter(UseWindowsPowerShell) 751 .AddParameter(WarningAction, SilentlyContinue) 752 .AddStatement(); 753 #endif 754 755 string cmd = cmdlet; 756 ps.AddCommand(cmdlet); 757 758 if (parameters != null) 759 { 760 foreach (var p in parameters) 761 { 762 cmd += $" -{p.Key} {p.Value}"; 763 } 764 765 ps.AddParameters(parameters); 766 } 767 768 if (options != null) 769 { 770 foreach (var option in options) 771 { 772 cmd += $" -{option}"; 773 ps.AddParameter(option); 774 } 775 } 776 777 this.pwshCmdlet.Write(StreamType.Verbose, $"Executing Appx cmdlet {cmd}"); 778 result = ps.Invoke(); 779 }); 780 781 return result; 782 } 783 784 private bool IsStubPackageOptionPresent() 785 { 786 bool result = false; 787 this.pwshCmdlet.ExecuteInPowerShellThread( 788 () => 789 { 790 var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); 791 792 #if !POWERSHELL_WINDOWS 793 ps.AddCommand(ImportModule) 794 .AddParameter(Name, Appx) 795 .AddParameter(UseWindowsPowerShell) 796 .AddParameter(WarningAction, SilentlyContinue) 797 .AddStatement(); 798 #endif 799 800 var cmdInfo = ps.AddCommand(GetCommand) 801 .AddParameter(Name, AddAppxPackage) 802 .AddParameter(Module, Appx) 803 .Invoke<CommandInfo>() 804 .FirstOrDefault(); 805 806 result = cmdInfo != null && cmdInfo.Parameters.ContainsKey(StubPackageOption); 807 }); 808 809 return result; 810 } 811 } 812 }