From 955e8fceb9a300c86bd244ff8628157576ee8e46 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:47:20 -0800 Subject: [PATCH 01/16] Added winget package uri --- .../FeaturedApplicationsProvider.cs | 3 +- SampleExtension/Program.cs | 2 +- SampleExtension/RepositoryProvider.cs | 1 + SampleExtension/SampleExtension.csproj | 2 +- .../Models/WinGetPackageUri.cs | 121 ++++++++++++++++++ .../Models/WinGetPackageUriOptions.cs | 58 +++++++++ .../Models/WinGetPackageUriParameters.cs | 16 +++ 7 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs diff --git a/SampleExtension/FeaturedApplicationsProvider.cs b/SampleExtension/FeaturedApplicationsProvider.cs index 469a382891..0fbb062f9b 100644 --- a/SampleExtension/FeaturedApplicationsProvider.cs +++ b/SampleExtension/FeaturedApplicationsProvider.cs @@ -31,7 +31,8 @@ public GetFeaturedApplicationsResult GetApplications() { // Sample list of featured applications return new GetFeaturedApplicationsResult(new List() - { + { + "x-ms-winget://winget/Postman.Postman?version=10.20.0", "x-ms-winget://winget/Microsoft.VisualStudio.2022.Community", "x-ms-winget://winget/Microsoft.VisualStudioCode", "x-ms-winget://winget/Microsoft.PowerShell", diff --git a/SampleExtension/Program.cs b/SampleExtension/Program.cs index 965024af98..e19674c54f 100644 --- a/SampleExtension/Program.cs +++ b/SampleExtension/Program.cs @@ -14,7 +14,7 @@ public static void Main(string[] args) { if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") { - using ExtensionServer server = new (); + using ExtensionServer server = new(); var extensionDisposedEvent = new ManualResetEvent(false); var extensionInstance = new SampleExtension(extensionDisposedEvent); diff --git a/SampleExtension/RepositoryProvider.cs b/SampleExtension/RepositoryProvider.cs index 2ff4dbe6b3..7125da5cff 100644 --- a/SampleExtension/RepositoryProvider.cs +++ b/SampleExtension/RepositoryProvider.cs @@ -7,6 +7,7 @@ using Windows.Storage.Streams; namespace SampleExtension; + internal sealed class RepositoryProvider : IRepositoryProvider { public string DisplayName => $"Sample {nameof(RepositoryProvider)}"; diff --git a/SampleExtension/SampleExtension.csproj b/SampleExtension/SampleExtension.csproj index 2ef4c992b9..9c965fb4ec 100644 --- a/SampleExtension/SampleExtension.csproj +++ b/SampleExtension/SampleExtension.csproj @@ -26,7 +26,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs new file mode 100644 index 0000000000..cab6da5acf --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.SetupFlow.Models; + +/// +/// Windows package manager (winget) package Uri +/// +/// Catalog name +/// Package Id +/// Uri options +public class WinGetPackageUri +{ + private WinGetPackageUri(string catalogName, string packageId, WinGetPackageUriOptions options = null) + { + CatalogName = catalogName; + PackageId = packageId; + Options = options ?? new(); + } + + /// + /// Windows package manager custom protocol scheme + /// + private const string Scheme = "x-ms-winget"; + + public string CatalogName { get; } + + public string PackageId { get; } + + public WinGetPackageUriOptions Options { get; } + + /// + /// Create a package Uri from a Uri + /// + /// Uri + /// Output package Uri + /// True if the Uri is a valid winget package Uri + public static bool TryCreate(Uri uri, out WinGetPackageUri packageUri) + { + // Ensure the Uri is not null + if (uri == null) + { + packageUri = null; + return false; + } + + // Ensure the Uri is a WinGet Uri + if (uri.Scheme == Scheme && uri.Segments.Length == 2) + { + var packageId = uri.Segments[1]; + var catalogUriName = uri.Host; + WinGetPackageUriOptions packageUriOptions = new(uri.Query); + packageUri = new(catalogUriName, packageId, packageUriOptions); + return true; + } + + packageUri = null; + return false; + } + + /// + /// Create a package Uri from a string + /// + /// String Uri + /// Output package Uri + /// True if the string Uri is a valid winget package Uri + public static bool TryCreate(string stringUri, out WinGetPackageUri packageUri) + { + // Ensure the string is a valid Uri + packageUri = null; + return Uri.TryCreate(stringUri, UriKind.Absolute, out var uri) && TryCreate(uri, out packageUri); + } + + /// + /// Create a package Uri from a string + /// + /// Include parameters + /// Uri string + public string ToString(WinGetPackageUriParameters includeParameters) + { + var queryString = Options.ToString(includeParameters); + return $"{Scheme}://{CatalogName}/{PackageId}{queryString}"; + } + + /// + /// Check if the package Uri is equal to the provided string Uri + /// + /// String Uri + /// Include parameters + /// True if the package Uri is equal to the string Uri + public bool Equals(string stringUri, WinGetPackageUriParameters includeParameters) + { + return TryCreate(stringUri, out var uri) && Equals(uri, includeParameters); + } + + /// + /// Check if the package Uri is equal to the provided package Uri + /// + /// Package Uri + /// Include parameters + /// True if the package Uri is equal to the Uri + public bool Equals(WinGetPackageUri packageUri, WinGetPackageUriParameters includeParameters) + { + if (packageUri == null) + { + return false; + } + + return CatalogName == packageUri.CatalogName && + PackageId == packageUri.PackageId && + Options.Equals(packageUri.Options, includeParameters); + } + + public override bool Equals(object obj) => Equals(obj as WinGetPackageUri, WinGetPackageUriParameters.All); + + public override string ToString() => ToString(WinGetPackageUriParameters.All); + + public override int GetHashCode() => ToString().GetHashCode(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs new file mode 100644 index 0000000000..f1a3fbadbb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Web; + +namespace DevHome.SetupFlow.Models; + +/// +/// Windows package manager (winget) package Uri options +/// +public sealed class WinGetPackageUriOptions +{ + // Query parameter names + private const string VersionQueryParameter = "version"; + + internal WinGetPackageUriOptions(string packageUriQuery = null) + { + var queryParams = HttpUtility.ParseQueryString(packageUriQuery ?? string.Empty); + Version = queryParams.Get(VersionQueryParameter) ?? string.Empty; + } + + public string Version { get; } + + public string ToString(WinGetPackageUriParameters includeParameters) + { + var queryParams = HttpUtility.ParseQueryString(string.Empty); + + // Add version + if (includeParameters.HasFlag(WinGetPackageUriParameters.Version) && !string.IsNullOrWhiteSpace(Version)) + { + queryParams.Add(VersionQueryParameter, Version); + } + + return queryParams.Count > 0 ? $"?{queryParams}" : string.Empty; + } + + public bool Equals(WinGetPackageUriOptions options, WinGetPackageUriParameters includeParameters) + { + if (options == null) + { + return false; + } + + // Check version + if (includeParameters.HasFlag(WinGetPackageUriParameters.Version) && Version != options.Version) + { + return false; + } + + return true; + } + + public override string ToString() => ToString(WinGetPackageUriParameters.All); + + public override bool Equals(object obj) => Equals(obj as WinGetPackageUriOptions, WinGetPackageUriParameters.All); + + public override int GetHashCode() => ToString().GetHashCode(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs new file mode 100644 index 0000000000..806c00abb4 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.SetupFlow.Models; + +[Flags] +public enum WinGetPackageUriParameters +{ + None = 0, + Version = 1 << 0, + + // Add all parameters here + All = Version, +} From b4a602e87df8380c625e0a853abd828063f8a1f4 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:23:07 -0800 Subject: [PATCH 02/16] Added UT for winget package uri --- .../WinGetPackageUriTests.cs | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs new file mode 100644 index 0000000000..f3da2494c6 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Models; + +namespace DevHome.SetupFlow.UnitTest; + +[TestClass] +public class WinGetPackageUriTests +{ + [TestMethod] + [DataRow("x-ms-winget://catalog/package")] + [DataRow("x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?not_supported=1")] + public void TryCreate_ValidUri_ReturnsTrue(string packageStringUri) + { + // Arrange + var uri = new Uri(packageStringUri); + + // Act + var result = WinGetPackageUri.TryCreate(uri, out var packageUri); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("catalog", packageUri.CatalogName); + Assert.AreEqual("package", packageUri.PackageId); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package")] + [DataRow("x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?not_supported=1")] + public void TryCreate_ValidStringUri_ReturnsTrue(string packageStringUri) + { + // Act + var result = WinGetPackageUri.TryCreate(packageStringUri, out var packageUri); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("catalog", packageUri.CatalogName); + Assert.AreEqual("package", packageUri.PackageId); + } + + [TestMethod] + public void TryCreate_NullStringUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate(null as string, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_NullUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate(null as Uri, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_InvalidUri_ReturnsFalse() + { + // Arrange + var uri = new Uri("https://www.microsoft.com"); + + // Act + var result = WinGetPackageUri.TryCreate(uri, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_InvalidStringUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate("https://www.microsoft.com", out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All, "x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.Version, "x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.None, "x-ms-winget://catalog/package")] + public void ToString_IncludeParameters_ReturnsUriString( + string packageStringUri, + WinGetPackageUriParameters includeParameters, + string toString) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri, out var packageUri); + + // Act + var result = packageUri.ToString(includeParameters); + + // Assert + Assert.AreEqual(toString, result); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.Version)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.None)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=2", WinGetPackageUriParameters.None)] + public void Equals_Uri_ReturnsTrue(string packageStringUri1, string packageStringUri2, WinGetPackageUriParameters includeParameters) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri1, out var packageUri1); + WinGetPackageUri.TryCreate(packageStringUri2, out var packageUri2); + + // Act + var result = packageUri1.Equals(packageUri2, includeParameters); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog2/package1?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package2?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package1?version=2", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package1?version=2", WinGetPackageUriParameters.Version)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog2/package1?version=1", WinGetPackageUriParameters.None)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package2?version=1", WinGetPackageUriParameters.None)] + public void Equals_Uri_ReturnsFalse(string packageStringUri1, string packageStringUri2, WinGetPackageUriParameters includeParameters) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri1, out var packageUri1); + WinGetPackageUri.TryCreate(packageStringUri2, out var packageUri2); + + // Act + var result = packageUri1.Equals(packageUri2, includeParameters); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void Equals_NullUri_ReturnsFalse() + { + // Arrange + WinGetPackageUri.TryCreate("x-ms-winget://catalog/package", out var packageUri); + + // Act + var result = packageUri.Equals(null as WinGetPackageUri, WinGetPackageUriParameters.All); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void Equals_UriAndStringUri_ReturnsTrue() + { + // Arrange + WinGetPackageUri.TryCreate("x-ms-winget://catalog/package?version=1", out var packageUri); + + // Act + var result = packageUri.Equals("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All); + + // Assert + Assert.IsTrue(result); + } +} From e5a758f6f2118c63074d33ac2a43b224c3fbf23d Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:52:22 -0800 Subject: [PATCH 03/16] Init changes --- .../Models/IWinGetPackage.cs | 20 +- .../DevHome.SetupFlow/Models/WinGetPackage.cs | 12 +- .../Models/WinGetPackageUri.cs | 2 +- .../Services/IWindowsPackageManager.cs | 12 +- .../Services/WinGet/IWinGetOperations.cs | 5 +- .../Services/WinGet/IWinGetPackageCache.cs | 7 +- .../WinGet/IWinGetPackageInstaller.cs | 2 +- .../Services/WinGet/IWinGetProtocolParser.cs | 22 +- .../Operations/IWinGetGetPackageOperation.cs | 3 +- .../Operations/IWinGetInstallOperation.cs | 10 +- .../Operations/WinGetGetPackageOperation.cs | 38 +- .../Operations/WinGetInstallOperation.cs | 23 +- .../Services/WinGet/WinGetOperations.cs | 5 +- .../Services/WinGet/WinGetPackageInstaller.cs | 38 +- .../Services/WinGet/WinGetProtocolParser.cs | 37 +- .../WinGetFeaturedApplicationsDataSource.cs | 12 +- .../Services/WinGetPackageDataSource.cs | 2 +- .../Services/WinGetPackageJsonDataSource.cs | 328 ++++++++++-------- .../WinGetPackageRestoreDataSource.cs | 2 +- .../Services/WindowsPackageManager.cs | 13 +- .../Styles/AppManagement_ThemeResources.xaml | 8 + .../ViewModels/PackageViewModel.cs | 24 +- .../Views/PackageCatalogView.xaml | 2 +- .../DevHome.SetupFlow/Views/PackageView.xaml | 21 +- 24 files changed, 331 insertions(+), 317 deletions(-) diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs index eaf61e0a76..2026a01ac6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using DevHome.SetupFlow.Common.WindowsPackageManager; using DevHome.SetupFlow.Services; using Windows.Storage.Streams; @@ -57,11 +58,17 @@ public string Name } /// - /// Gets the version of the package which could be of any format supported - /// by WinGet package manager (e.g. alpha-numeric, 'Unknown', '1-preview, etc...). - /// + /// Gets the installed version of the package or null if the package is not installed /// - public string Version + public string InstalledVersion + { + get; + } + + /// + /// Gets the list of available versions for the package + /// + public IReadOnlyList AvailableVersions { get; } @@ -127,12 +134,13 @@ public string InstallationNotes /// /// Windows package manager service /// String resource service - /// WinGet factory + /// Version to install + /// Activity id /// Task object for installing this package InstallPackageTask CreateInstallTask( IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, - WindowsPackageManagerFactory wingetFactory, + string version, Guid activityId); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs index 97ff5ca0d7..fdbee447a4 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs @@ -2,9 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using DevHome.SetupFlow.Common.Helpers; -using DevHome.SetupFlow.Common.WindowsPackageManager; using DevHome.SetupFlow.Services; using Microsoft.Management.Deployment; using Windows.Storage.Streams; @@ -28,7 +29,8 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) CatalogName = package.DefaultInstallVersion.PackageCatalog.Info.Name; UniqueKey = new(Id, CatalogId); Name = package.Name; - Version = package.DefaultInstallVersion.Version; + InstalledVersion = package.InstalledVersion.Version; + AvailableVersions = package.AvailableVersions.Select(v => v.Version).ToList(); IsInstalled = package.InstalledVersion != null; IsElevationRequired = requiresElevated; PackageUrl = GetMetadataValue(package, metadata => new Uri(metadata.PackageUrl), nameof(CatalogPackageMetadata.PackageUrl), null); @@ -47,7 +49,9 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) public string Name { get; } - public string Version { get; } + public string InstalledVersion { get; } + + public IReadOnlyList AvailableVersions { get; } public bool IsInstalled { get; } @@ -68,7 +72,7 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) public InstallPackageTask CreateInstallTask( IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, - WindowsPackageManagerFactory wingetFactory, + string version, Guid activityId) => new(wpm, stringResource, this, activityId); /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs index cab6da5acf..063bb72ab9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs @@ -13,7 +13,7 @@ namespace DevHome.SetupFlow.Models; /// Uri options public class WinGetPackageUri { - private WinGetPackageUri(string catalogName, string packageId, WinGetPackageUriOptions options = null) + internal WinGetPackageUri(string catalogName, string packageId, WinGetPackageUriOptions options = null) { CatalogName = catalogName; PackageId = packageId; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs index 23079891f7..cccbb46b85 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs @@ -22,10 +22,10 @@ public interface IWindowsPackageManager public Task InitializeAsync(); /// - public Task InstallPackageAsync(IWinGetPackage package); + public Task InstallPackageAsync(WinGetPackageUri packageUri); /// - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); /// public Task> SearchAsync(string query, uint limit); @@ -46,14 +46,14 @@ public interface IWindowsPackageManager public bool IsWinGetPackage(IWinGetPackage package); /// - public Uri CreatePackageUri(IWinGetPackage package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package); /// - public Uri CreateWinGetCatalogPackageUri(string packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs index b2c220ec27..b2442d87d9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -11,10 +10,10 @@ namespace DevHome.SetupFlow.Services.WinGet.Operations; internal interface IWinGetOperations { /// " - public Task InstallPackageAsync(IWinGetPackage package); + public Task InstallPackageAsync(WinGetPackageUri packageUri); /// " - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); /// " public Task> SearchAsync(string query, uint limit); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs index 35c0433158..402f02f3c7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using DevHome.SetupFlow.Models; @@ -18,7 +17,7 @@ internal interface IWinGetPackageCache /// Package URIs to find /// Output package URIs not found /// List of packages found - public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound); + public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound); /// /// Try to get a package in the cache. @@ -26,7 +25,7 @@ internal interface IWinGetPackageCache /// Package URI to find /// Output package /// True if the package was found, false otherwise. - public bool TryGetPackage(Uri packageUri, out IWinGetPackage package); + public bool TryGetPackage(WinGetPackageUri packageUri, out IWinGetPackage package); /// /// Try to add a package to the cache. @@ -34,7 +33,7 @@ internal interface IWinGetPackageCache /// Package URI to add /// Package to add /// True if the package was added, false otherwise. - public bool TryAddPackage(Uri packageUri, IWinGetPackage package); + public bool TryAddPackage(WinGetPackageUri packageUri, IWinGetPackage package); /// /// Clear the cache. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs index 6aa290e03f..b2151809e6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs @@ -14,5 +14,5 @@ internal interface IWinGetPackageInstaller /// Catalog from which to install the package /// Package id to install /// Result of the installation - public Task InstallPackageAsync(WinGetCatalog catalog, string packageId); + public Task InstallPackageAsync(WinGetCatalog catalog, string packageId, string version); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs index d479ae1406..c26026c934 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -14,21 +13,21 @@ internal interface IWinGetProtocolParser /// /// Package /// Package uri - public Uri CreatePackageUri(IWinGetPackage package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package); /// /// Create a winget catalog package uri from a package id /// /// Package id /// Package uri - public Uri CreateWinGetCatalogPackageUri(string packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId); /// /// Create a Microsoft store catalog package uri from a package id /// /// Package id /// Package uri - public Uri CreateMsStoreCatalogPackageUri(string packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId); /// /// Create a custom catalog package uri from a package id and catalog name @@ -36,21 +35,14 @@ internal interface IWinGetProtocolParser /// Package id /// Catalog name /// Package uri - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName); - - /// - /// Get the package id and catalog from a package uri - /// - /// Input package uri - /// Package id and catalog, or null if the URI protocol is inaccurate - public WinGetProtocolParserResult ParsePackageUri(Uri packageUri); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName); /// /// Resolve a catalog from a parser result /// - /// Parser result + /// Package uri /// Catalog - public Task ResolveCatalogAsync(WinGetProtocolParserResult result); + public Task ResolveCatalogAsync(WinGetPackageUri packageUri); /// /// Create a package uri from a package id and catalog @@ -58,5 +50,5 @@ internal interface IWinGetProtocolParser /// Package id /// Catalog /// Package uri - public Uri CreatePackageUri(string packageId, WinGetCatalog catalog); + public WinGetPackageUri CreatePackageUri(string packageId, WinGetCatalog catalog); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs index 4f313441cd..9ce3b7eaa9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -15,5 +14,5 @@ internal interface IWinGetGetPackageOperation /// /// List of package uri /// List of winget package matches - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs index e041be8dd0..5c0c51fa1a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -9,17 +8,10 @@ namespace DevHome.SetupFlow.Services.WinGet.Operations; internal interface IWinGetInstallOperation { - /// - /// Install a package on the user's machine. - /// - /// Package to install - /// Install package result - public Task InstallPackageAsync(IWinGetPackage package); - /// /// Installs a package from a URI. /// /// Uri of the package to install. /// Result of the installation. - public Task InstallPackageAsync(Uri packageUri); + public Task InstallPackageAsync(WinGetPackageUri packageUri); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs index 811464ee78..681918dfbf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs @@ -33,7 +33,7 @@ public WinGetGetPackageOperation( } /// - public async Task> GetPackagesAsync(IList packageUris) + public async Task> GetPackagesAsync(IList packageUris) { // Remove duplicates (optimization to prevent querying the same package multiple times) var distinctPackageUris = packageUris.Distinct(); @@ -45,7 +45,8 @@ public async Task> GetPackagesAsync(IList packageUris // Get packages grouped by catalog var getPackagesTasks = new List>>(); - foreach (var parsedUrisGroup in GroupParsedUrisByCatalog(packageUrisToQuery)) + var groupedParsedUris = packageUrisToQuery.GroupBy(p => p.CatalogName).Select(p => p.ToList()).ToList(); + foreach (var parsedUrisGroup in groupedParsedUris) { if (parsedUrisGroup.Count != 0) { @@ -55,7 +56,7 @@ public async Task> GetPackagesAsync(IList packageUris // All parsed URIs in the group have the same catalog, resolve catalog from the first entry var firstParsedUri = parsedUrisGroup.First(); var packageIds = parsedUrisGroup.Select(p => p.PackageId).ToHashSet(); - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Getting packages [{string.Join(", ", packageIds)}] from parsed uri catalog name: {firstParsedUri.CatalogUriName}"); + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Getting packages [{string.Join(", ", packageIds)}] from parsed uri catalog name: {firstParsedUri.CatalogName}"); // Get packages from the catalog var catalog = await _protocolParser.ResolveCatalogAsync(firstParsedUri); @@ -75,13 +76,13 @@ public async Task> GetPackagesAsync(IList packageUris var unorderedPackagesMap = getPackagesTasks .SelectMany(p => p.Result) .Concat(cachedPackages) - .ToDictionary(p => _protocolParser.CreatePackageUri(p), p => p); + .ToDictionary(p => _protocolParser.CreatePackageUri(p).ToString(WinGetPackageUriParameters.None), p => p); // Order packages by the order of the input URIs using a dictionary var orderedPackages = new List(); foreach (var packageUri in packageUris) { - if (unorderedPackagesMap.TryGetValue(packageUri, out var package)) + if (unorderedPackagesMap.TryGetValue(packageUri.ToString(WinGetPackageUriParameters.None), out var package)) { orderedPackages.Add(package); } @@ -93,31 +94,4 @@ public async Task> GetPackagesAsync(IList packageUris return orderedPackages; } - - /// - /// Group packages by their catalogs - /// - /// Package URIs - /// Dictionary of package ids by catalog - private List> GroupParsedUrisByCatalog(IEnumerable packageUriSet) - { - var parsedUris = new List(); - - // 1. Parse all package URIs and log invalid ones - foreach (var packageUri in packageUriSet) - { - var uriInfo = _protocolParser.ParsePackageUri(packageUri); - if (uriInfo != null) - { - parsedUris.Add(uriInfo); - } - else - { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Failed to get URI details from '{packageUri}'"); - } - } - - // 2. Group package ids by catalog - return parsedUris.GroupBy(p => p.CatalogUriName).Select(p => p.ToList()).ToList(); - } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs index a004159b61..7d42fc27f3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -30,28 +29,12 @@ public WinGetInstallOperation( } /// - public async Task InstallPackageAsync(IWinGetPackage package) + public async Task InstallPackageAsync(WinGetPackageUri packageUri) { return await _recovery.DoWithRecoveryAsync(async () => { - var catalog = await _catalogConnector.GetPackageCatalogAsync(package); - return await _packageInstaller.InstallPackageAsync(catalog, package.Id); - }); - } - - /// - public async Task InstallPackageAsync(Uri packageUri) - { - var parsedPackageUri = _protocolParser.ParsePackageUri(packageUri); - if (parsedPackageUri == null) - { - throw new ArgumentException($"Invalid package URI ${packageUri}"); - } - - return await _recovery.DoWithRecoveryAsync(async () => - { - var catalog = await _protocolParser.ResolveCatalogAsync(parsedPackageUri); - return await _packageInstaller.InstallPackageAsync(catalog, parsedPackageUri.PackageId); + var catalog = await _protocolParser.ResolveCatalogAsync(packageUri); + return await _packageInstaller.InstallPackageAsync(catalog, packageUri.PackageId, packageUri.Options.Version); }); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs index 4145b4fae3..8ddc086943 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -25,10 +24,10 @@ public WinGetOperations( } /// - public async Task InstallPackageAsync(IWinGetPackage package) => await _installOperation.InstallPackageAsync(package); + public async Task InstallPackageAsync(WinGetPackageUri packageUri) => await _installOperation.InstallPackageAsync(packageUri); /// - public async Task> GetPackagesAsync(IList packageUris) => await _getPackageOperation.GetPackagesAsync(packageUris); + public async Task> GetPackagesAsync(IList packageUris) => await _getPackageOperation.GetPackagesAsync(packageUris); /// public async Task> SearchAsync(string query, uint limit) => await _searchOperation.SearchAsync(query, limit); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs index a2b8908228..630d1c3392 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs @@ -28,7 +28,8 @@ public WinGetPackageInstaller(WindowsPackageManagerFactory wingetFactory, IWinGe } /// - public async Task InstallPackageAsync(WinGetCatalog catalog, string packageId) + /// TODO: Consider using install options instead of 'version' parameter + public async Task InstallPackageAsync(WinGetCatalog catalog, string packageId, string version) { if (catalog == null) { @@ -45,7 +46,7 @@ public async Task InstallPackageAsync(WinGetCatalog catalo // 2. Install package Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Starting package installation for {packageId} from catalog {catalog.GetDescriptiveName()}"); - var installResult = await InstallPackageInternalAsync(package); + var installResult = await InstallPackageInternalAsync(package, version); var extendedErrorCode = installResult.ExtendedErrorCode?.HResult ?? HRESULT.S_OK; var installErrorCode = installResult.GetValueOrDefault(res => res.InstallerErrorCode, HRESULT.S_OK); // WPM API V4 @@ -69,11 +70,42 @@ public async Task InstallPackageAsync(WinGetCatalog catalo /// /// Package to install /// Install result - private async Task InstallPackageInternalAsync(CatalogPackage package) + private async Task InstallPackageInternalAsync(CatalogPackage package, string version) { var installOptions = _wingetFactory.CreateInstallOptions(); installOptions.PackageInstallMode = PackageInstallMode.Silent; + if (TryFindVersion(package, version, out var versionId)) + { + installOptions.PackageVersionId = versionId; + } + else + { + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Specified version not found '{version}'. Falling back to default install version {package.DefaultInstallVersion.Version}"); + } + var packageManager = _wingetFactory.CreatePackageManager(); return await packageManager.InstallPackageAsync(package, installOptions).AsTask(); } + + /// + /// Try to find a specific version in the list of available versions for a package. + /// + /// Target package + /// Version to find + /// Found version + /// True if the version was found, false otherwise + private bool TryFindVersion(CatalogPackage package, string version, out PackageVersionId versionId) + { + for (var i = 0; i < package.AvailableVersions.Count; i++) + { + if (package.AvailableVersions[i].Version == version) + { + versionId = package.AvailableVersions[i]; + return true; + } + } + + versionId = null; + return false; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs index bba9c9bbd0..f8502743dc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs @@ -20,11 +20,6 @@ public WinGetProtocolParser(IWinGetCatalogConnector catalogConnector) _catalogConnector = catalogConnector; } - /// - /// Windows package manager custom protocol scheme - /// - private const string Scheme = "x-ms-winget"; - /// /// Reserved URI name for the WinGet catalog /// @@ -36,22 +31,9 @@ public WinGetProtocolParser(IWinGetCatalogConnector catalogConnector) private const string ReservedMsStoreCatalogURIName = "msstore"; /// - public WinGetProtocolParserResult ParsePackageUri(Uri packageUri) + public async Task ResolveCatalogAsync(WinGetPackageUri packageUri) { - if (packageUri.Scheme == Scheme && packageUri.Segments.Length == 2) - { - var packageId = packageUri.Segments[1]; - var catalogUriName = packageUri.Host; - return new(packageId, catalogUriName); - } - - return null; - } - - /// - public async Task ResolveCatalogAsync(WinGetProtocolParserResult result) - { - var catalogName = result.CatalogUriName; + var catalogName = packageUri.CatalogName; // 'winget' catalog if (catalogName == ReservedWingetCatalogURIName) @@ -70,16 +52,16 @@ public async Task ResolveCatalogAsync(WinGetProtocolParserResult } /// - public Uri CreateWinGetCatalogPackageUri(string packageId) => new($"{Scheme}://{ReservedWingetCatalogURIName}/{packageId}"); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId) => new(ReservedWingetCatalogURIName, packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId) => new($"{Scheme}://{ReservedMsStoreCatalogURIName}/{packageId}"); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId) => new(ReservedMsStoreCatalogURIName, packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName) => new($"{Scheme}://{catalogName}/{packageId}"); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName) => new(catalogName, packageId); /// - public Uri CreatePackageUri(string packageId, WinGetCatalog catalog) + public WinGetPackageUri CreatePackageUri(string packageId, WinGetCatalog catalog) { return catalog.Type switch { @@ -91,10 +73,5 @@ public Uri CreatePackageUri(string packageId, WinGetCatalog catalog) } /// - public Uri CreatePackageUri(IWinGetPackage package) - { - return CreateCustomCatalogPackageUri(package.Id, package.CatalogName); - } + public WinGetPackageUri CreatePackageUri(IWinGetPackage package) => CreateCustomCatalogPackageUri(package.Id, package.CatalogName); } - -public record class WinGetProtocolParserResult(string PackageId, string CatalogUriName); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index 81979549d3..abc9939f8f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -142,18 +142,18 @@ private async Task LoadCatalogAsync(IFeaturedApplicationsGroup g /// /// List of package URI strings /// List of package URIs - private List ParseURIs(IReadOnlyList uriStrings) + private List ParseURIs(IReadOnlyList uriStrings) { - var result = new List(); - foreach (var app in uriStrings) + var result = new List(); + foreach (var uriString in uriStrings) { - if (Uri.TryCreate(app, UriKind.Absolute, out var uri)) + if (WinGetPackageUri.TryCreate(uriString, out var packageUri)) { - result.Add(uri); + result.Add(packageUri); } else { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Invalid package uri: {app}"); + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Invalid package uri: {uriString}"); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs index 955695bee6..0905c49155 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs @@ -44,7 +44,7 @@ public WinGetPackageDataSource(IWindowsPackageManager wpm) /// Input type /// List of package URIs /// List of packages - protected async Task> GetPackagesAsync(IList packageUris) + protected async Task> GetPackagesAsync(IList packageUris) { List result = new(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs index b45274b324..2712229692 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs @@ -1,108 +1,138 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using DevHome.Common.Extensions; -using DevHome.SetupFlow.Common.Helpers; -using DevHome.SetupFlow.Models; -using Windows.Storage; -using Windows.Storage.Streams; - -namespace DevHome.SetupFlow.Services; - -/// -/// Class for loading package catalogs from a JSON data source -/// -public class WinGetPackageJsonDataSource : WinGetPackageDataSource -{ - /// - /// Class for deserializing a JSON winget package - /// - private sealed class JsonWinGetPackage - { - public Uri Uri { get; set; } - - public string Icon { get; set; } - } - - /// - /// Class for deserializing a JSON package catalog with package ids from - /// winget - /// - private sealed class JsonWinGetPackageCatalog - { - public string NameResourceKey { get; set; } - - public string DescriptionResourceKey { get; set; } - - public IList WinGetPackages { get; set; } - } - - private readonly ISetupFlowStringResource _stringResource; - private readonly string _fileName; - private readonly JsonSerializerOptions jsonSerializerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip }; - private IList _jsonCatalogs = new List(); - - public override int CatalogCount => _jsonCatalogs.Count; - - public WinGetPackageJsonDataSource( - ISetupFlowStringResource stringResource, - IWindowsPackageManager wpm, - string fileName) - : base(wpm) - { - _stringResource = stringResource; - _fileName = fileName; - } - - public async override Task InitializeAsync() - { - // Open and deserialize JSON file - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Reading package list from JSON file {_fileName}"); - using var fileStream = File.OpenRead(_fileName); - - _jsonCatalogs = await JsonSerializer.DeserializeAsync>(fileStream, jsonSerializerOptions); - } - - public async override Task> LoadCatalogsAsync() - { - var result = new List(); - foreach (var jsonCatalog in _jsonCatalogs) - { - var packageCatalog = await LoadCatalogAsync(jsonCatalog); - if (packageCatalog != null) - { - result.Add(packageCatalog); - } - } - - return result; - } - - /// - /// Load a package catalog with the list of winget packages sorted based on - /// the input JSON catalog - /// - /// JSON catalog - /// Package catalog - private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jsonCatalog) - { - var catalogName = _stringResource.GetLocalized(jsonCatalog.NameResourceKey); - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Attempting to read JSON package catalog {catalogName}"); - - try - { - var packages = await GetPackagesAsync(jsonCatalog.WinGetPackages.Select(p => p.Uri).ToList()); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.Common.Helpers; +using DevHome.SetupFlow.Models; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace DevHome.SetupFlow.Services; + +/// +/// Class for loading package catalogs from a JSON data source +/// +public class WinGetPackageJsonDataSource : WinGetPackageDataSource +{ + /// + /// Class for deserializing a JSON winget package + /// + private sealed class JsonWinGetPackage + { + public Uri Uri { get; set; } + + public string Icon { get; set; } + + public WinGetPackageUri GetPackageUri() + { + if (WinGetPackageUri.TryCreate(Uri, out var packageUri)) + { + return packageUri; + } + + return null; + } + } + + /// + /// Class for deserializing a JSON package catalog with package ids from + /// winget + /// + private sealed class JsonWinGetPackageCatalog + { + public string NameResourceKey { get; set; } + + public string DescriptionResourceKey { get; set; } + + public IList WinGetPackages { get; set; } + } + + private readonly ISetupFlowStringResource _stringResource; + private readonly string _fileName; + private readonly JsonSerializerOptions jsonSerializerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip }; + private IList _jsonCatalogs = new List(); + + public override int CatalogCount => _jsonCatalogs.Count; + + public WinGetPackageJsonDataSource( + ISetupFlowStringResource stringResource, + IWindowsPackageManager wpm, + string fileName) + : base(wpm) + { + _stringResource = stringResource; + _fileName = fileName; + } + + public async override Task InitializeAsync() + { + // Open and deserialize JSON file + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Reading package list from JSON file {_fileName}"); + using var fileStream = File.OpenRead(_fileName); + + _jsonCatalogs = await JsonSerializer.DeserializeAsync>(fileStream, jsonSerializerOptions); + } + + public async override Task> LoadCatalogsAsync() + { + var result = new List(); + foreach (var jsonCatalog in _jsonCatalogs) + { + var packageCatalog = await LoadCatalogAsync(jsonCatalog); + if (packageCatalog != null) + { + result.Add(packageCatalog); + } + } + + return result; + } + + private List GetPackageUris(IList jsonPackages) + { + var result = new List(); + foreach (var jsonPackage in jsonPackages) + { + var packageUri = jsonPackage.GetPackageUri(); + if (packageUri != null) + { + result.Add(packageUri); + } + else + { + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Skipping {jsonPackage.Uri} because it is not a valid winget package uri"); + } + } + + return result; + } + + /// + /// Load a package catalog with the list of winget packages sorted based on + /// the input JSON catalog + /// + /// JSON catalog + /// Package catalog + private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jsonCatalog) + { + var catalogName = _stringResource.GetLocalized(jsonCatalog.NameResourceKey); + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Attempting to read JSON package catalog {catalogName}"); + + try + { + var packageUris = GetPackageUris(jsonCatalog.WinGetPackages); + var packages = await GetPackagesAsync(packageUris); Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Obtaining icon information for JSON packages: [{string.Join(", ", packages.Select(p => $"({p.Name}, {p.CatalogName})"))}]"); foreach (var package in packages) { var packageUri = WindowsPackageManager.CreatePackageUri(package); - var jsonPackage = jsonCatalog.WinGetPackages.FirstOrDefault(p => packageUri == p.Uri); + var jsonPackage = jsonCatalog.WinGetPackages.FirstOrDefault(p => packageUri.Equals(p.GetPackageUri(), WinGetPackageUriParameters.None)); if (jsonPackage != null) { var icon = await GetJsonApplicationIconAsync(jsonPackage); @@ -111,51 +141,51 @@ private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jso } } - if (packages.Any()) - { - return new PackageCatalog() - { - Name = catalogName, - Description = _stringResource.GetLocalized(jsonCatalog.DescriptionResourceKey), - Packages = packages.ToReadOnlyCollection(), - }; - } - else - { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"JSON package catalog [{catalogName}] is empty"); - } - } - catch (Exception e) - { - Log.Logger?.ReportError(Log.Component.AppManagement, $"Error loading packages from winget catalog.", e); - } - - return null; - } - - private async Task GetJsonApplicationIconAsync(JsonWinGetPackage package) - { - try - { - if (!string.IsNullOrEmpty(package.Icon)) - { - // Load icon from application assets - var iconFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(package.Icon)); - var icon = await iconFile.OpenAsync(FileAccessMode.Read); - - // Ensure stream is not empty to prevent rendering an empty image - if (icon.Size > 0) - { - return icon; - } - } - } - catch (Exception e) - { - Log.Logger?.ReportError(Log.Component.AppManagement, $"Failed to get icon for JSON package {package.Uri}.", e); - } - - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"No icon found for JSON package {package.Uri}. A default one will be provided."); - return null; - } -} + if (packages.Any()) + { + return new PackageCatalog() + { + Name = catalogName, + Description = _stringResource.GetLocalized(jsonCatalog.DescriptionResourceKey), + Packages = packages.ToReadOnlyCollection(), + }; + } + else + { + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"JSON package catalog [{catalogName}] is empty"); + } + } + catch (Exception e) + { + Log.Logger?.ReportError(Log.Component.AppManagement, $"Error loading packages from winget catalog.", e); + } + + return null; + } + + private async Task GetJsonApplicationIconAsync(JsonWinGetPackage package) + { + try + { + if (!string.IsNullOrEmpty(package.Icon)) + { + // Load icon from application assets + var iconFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(package.Icon)); + var icon = await iconFile.OpenAsync(FileAccessMode.Read); + + // Ensure stream is not empty to prevent rendering an empty image + if (icon.Size > 0) + { + return icon; + } + } + } + catch (Exception e) + { + Log.Logger?.ReportError(Log.Component.AppManagement, $"Failed to get icon for JSON package {package.Uri}.", e); + } + + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"No icon found for JSON package {package.Uri}. A default one will be provided."); + return null; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs index 3330ad8e85..de0518f02e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs @@ -157,7 +157,7 @@ private string GetDescription() /// Application information /// Package URI /// All restored applications are from winget catalog - private Uri GetPackageUri(IRestoreApplicationInfo appInfo) + private WinGetPackageUri GetPackageUri(IRestoreApplicationInfo appInfo) { return WindowsPackageManager.CreateWinGetCatalogPackageUri(appInfo.Id); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs index 9073f29ee1..3945c48371 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -46,10 +45,10 @@ public async Task InitializeAsync() } /// - public async Task InstallPackageAsync(IWinGetPackage package) => await _operations.InstallPackageAsync(package); + public async Task InstallPackageAsync(WinGetPackageUri packageUri) => await _operations.InstallPackageAsync(packageUri); /// - public async Task> GetPackagesAsync(IList packageUris) => await _operations.GetPackagesAsync(packageUris); + public async Task> GetPackagesAsync(IList packageUris) => await _operations.GetPackagesAsync(packageUris); /// public async Task> SearchAsync(string query, uint limit) => await _operations.SearchAsync(query, limit); @@ -70,14 +69,14 @@ public async Task InitializeAsync() public bool IsWinGetPackage(IWinGetPackage package) => _catalogConnector.IsWinGetPackage(package); /// - public Uri CreatePackageUri(IWinGetPackage package) => _protocolParser.CreatePackageUri(package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package) => _protocolParser.CreatePackageUri(package); /// - public Uri CreateWinGetCatalogPackageUri(string packageId) => _protocolParser.CreateWinGetCatalogPackageUri(packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId) => _protocolParser.CreateWinGetCatalogPackageUri(packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId) => _protocolParser.CreateMsStoreCatalogPackageUri(packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId) => _protocolParser.CreateMsStoreCatalogPackageUri(packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName) => _protocolParser.CreateCustomCatalogPackageUri(packageId, catalogName); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName) => _protocolParser.CreateCustomCatalogPackageUri(packageId, catalogName); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml index 734204943e..ab719d6aaf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml @@ -77,6 +77,14 @@ +