winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 24511e4f3d053d4f34f3157950e4e3a20599f039
parent 855942f56a2b6dff97d9ce54739249425b8faa2e
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Mon, 24 Feb 2025 15:03:51 -0800

Consider installer applicability in IsUpdateAvailable COM api (#5228)

Added e2e tests. Also added unit tests for rest source with multiple
manifest versions response.

Also fixed the Source Name logic in the PS output class to retrieve
matching source name for the package id.

Diffstat:
Msrc/AppInstallerCLIE2ETests/Helpers/TestCommon.cs | 6++----
Msrc/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs | 7+++++--
Msrc/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.1.0.0.0.yaml | 20++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.2.0.0.0.yaml | 20++++++++++++++++++++
Msrc/AppInstallerCLITests/RestInterface_1_0.cpp | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/YamlParser.cpp | 6+++---
Msrc/Microsoft.Management.Deployment/CatalogPackage.cpp | 54+++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 16++++++++++++++--
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 12++++++------
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/CatalogPackageExtensions.cs | 22+++++++++++++++++++++-
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs | 5+++--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSDownloadResult.cs | 7++++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSFoundCatalogPackage.cs | 6+++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs | 7++++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs | 5+++--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs | 7++++---
Msrc/WinGetUtilInterop.UnitTests/TestCollateral/V1_10ManifestMerged.yaml | 2+-
Msrc/WinGetUtilInterop.UnitTests/TestCollateral/V1_9ManifestMerged.yaml | 2+-
19 files changed, 330 insertions(+), 51 deletions(-)

diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -392,10 +392,8 @@ namespace AppInstallerCLIE2ETests.Helpers isAddedToPath = currentPathValue.Contains(portablePathValue); } - if (shouldExist) - { - RunAICLICommand("uninstall", $"--product-code {productCode} --force"); - } + // Always clean up as best effort. + RunAICLICommand("uninstall", $"--product-code {productCode} --force"); Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}"); Assert.AreEqual(shouldExist && !installDirectoryAddedToPath, symlinkExists, $"Expected portable symlink path: {symlinkPath}"); diff --git a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="UninstallInterop.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -277,7 +277,10 @@ namespace AppInstallerCLIE2ETests.Interop Assert.True(modifiedSymlinkInfo.Exists, "Modified symlink should still exist"); // Remove modified symlink as to not interfere with other tests - modifiedSymlinkInfo.Delete(); + modifiedSymlinkInfo.Delete(); + + // Uninstall again to clean up. + await this.packageManager.UninstallPackageAsync(searchResult.CatalogPackage, this.TestFactory.CreateUninstallOptions()); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="UpgradeInterop.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,7 +8,8 @@ namespace AppInstallerCLIE2ETests.Interop { using System; using System.Collections.Generic; - using System.IO; + using System.IO; + using System.Linq; using System.Threading.Tasks; using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; @@ -78,7 +79,7 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, but this time it should detect the installed version searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "1.0.0.0"); + Assert.AreEqual("1.0.0.0", searchResult.CatalogPackage.InstalledVersion?.Version); // Configure upgrade options var upgradeOptions = this.TestFactory.CreateInstallOptions(); @@ -90,7 +91,7 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, but this time it should detect the upgraded installed version searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "2.0.0.0"); + Assert.AreEqual("2.0.0.0", searchResult.CatalogPackage.InstalledVersion?.Version); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); } @@ -137,7 +138,7 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, it should have not been upgraded searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "1.0.0.0"); + Assert.AreEqual("1.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); } @@ -185,7 +186,7 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, but this time it should detect the upgraded installed version searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "2.0.0.0"); + Assert.AreEqual("2.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); } @@ -216,7 +217,7 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, but this time it should detect the installed version searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "1.0.0.0"); + Assert.AreEqual("1.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); // Configure upgrade options var upgradeOptions = this.TestFactory.CreateInstallOptions(); @@ -228,8 +229,71 @@ namespace AppInstallerCLIE2ETests.Interop // Find package again, but this time it should detect the upgraded installed version searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); - Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "3.0.0.0"); + Assert.AreEqual("3.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + /// <summary> + /// Tests IsUpdateAvailable. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task TestIsUpdateAvailable_ApplicableTrue() + { + // Find and install the test package. Install the version 1.0.0.0. + var installDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PreferredInstallLocation = installDir; + installOptions.PackageVersionId = First(searchResult.CatalogPackage.AvailableVersions, i => i.Version == "1.0.0.0"); + var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + + // Find package again, but this time it should detect the installed version. + searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + + // The installed version is 1.0.0.0. + Assert.AreEqual("1.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); + + // IsUpdateAvailable is true. + Assert.True(searchResult.CatalogPackage.IsUpdateAvailable); + + // Uninstall to clean up. + var uninstallOptions = this.TestFactory.CreateUninstallOptions(); + var uninstallResult = await this.packageManager.UninstallPackageAsync(searchResult.CatalogPackage, uninstallOptions); + } + + /// <summary> + /// Tests applicability check is performed for IsUpdateAvailable api. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task TestIsUpdateAvailable_ApplicableFalse() + { + // Find and install the test package. Install the version 1.0.0.0. + var installDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestUpgradeApplicability"); + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PreferredInstallLocation = installDir; + installOptions.PackageVersionId = First(searchResult.CatalogPackage.AvailableVersions, i => i.Version == "1.0.0.0"); + var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + + // Find package again, but this time it should detect the installed version. + searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestUpgradeApplicability"); + + // The installed version is 1.0.0.0. + Assert.AreEqual("1.0.0.0", searchResult.CatalogPackage.InstalledVersion.Version); + + // There is version 2.0.0.0 in the package available versions. + Assert.NotNull(First(searchResult.CatalogPackage.AvailableVersions, i => i.Version == "2.0.0.0")); + + // IsUpdateAvailable is false due to applicability check. Only arm64 in version 2.0.0.0. + Assert.False(searchResult.CatalogPackage.IsUpdateAvailable); + + // Uninstall to clean up. + var uninstallOptions = this.TestFactory.CreateUninstallOptions(); + var uninstallResult = await this.packageManager.UninstallPackageAsync(searchResult.CatalogPackage, uninstallOptions); } // Cannot use foreach or Linq for out-of-process IVector diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.1.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.1.0.0.0.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestUpgradeApplicability +Name: TestUpgradeApplicability +Version: 1.0.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{bfb0f666-99d5-433d-8a2e-32f31d4f8e48}' + Switches: + Custom: '/ProductID {bfb0f666-99d5-433d-8a2e-32f31d4f8e48} /DisplayName TestUpgradeApplicability' + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestUpgradeAvailableApi.2.0.0.0.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestUpgradeApplicability +Name: TestUpgradeApplicability +Version: 2.0.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: arm64 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{bfb0f666-99d5-433d-8a2e-32f31d4f8e48}' + Switches: + Custom: '/ProductID {bfb0f666-99d5-433d-8a2e-32f31d4f8e48} /DisplayName TestUpgradeApplicability' + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -53,6 +53,54 @@ namespace })delimiter"); } + utility::string_t GetManifestsResponse_MultipleVersions() + { + return _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + }, + { + "PackageVersion": "6.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + } + ] + } + })delimiter"); + } + struct GoodManifest_AllFields { utility::string_t GetSampleManifest_AllFields() @@ -440,6 +488,22 @@ TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource][Interface_1_0]") REQUIRE(manifest.Installers[0].Url == "https://installer.example.com/foobar.exe"); } +TEST_CASE("Search_Optimized_ManifestResponse_MultipleVersions", "[RestSource][Interface_1_0]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, GetManifestsResponse_MultipleVersions()) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Id, MatchType::Exact, "Foo.Bar" }; + request.Filters.emplace_back(std::move(filter)); + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search(request); + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Versions.size() == 2); + REQUIRE(result.Matches[0].Versions[0].VersionAndChannel.GetVersion().ToString() == "5.0.0"); + REQUIRE(result.Matches[0].Versions[0].Manifest); + REQUIRE(result.Matches[0].Versions[1].VersionAndChannel.GetVersion().ToString() == "6.0.0"); + REQUIRE(result.Matches[0].Versions[1].Manifest); +} + TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; @@ -481,6 +545,18 @@ TEST_CASE("GetManifests_GoodResponse_404AsEmpty", "[RestSource][Interface_1_0]") REQUIRE(manifests.size() == 0); } +TEST_CASE("GetManifests_GoodResponse_MultipleVersions", "[RestSource][Interface_1_0]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, GetManifestsResponse_MultipleVersions()) }; + Interface v1{ TestRestUriString, std::move(helper) }; + + // GetManifests + std::vector<Manifest> manifests = v1.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 2); + REQUIRE(manifests[0].Version == "5.0.0"); + REQUIRE(manifests[1].Version == "6.0.0"); +} + TEST_CASE("GetManifests_BadResponse_SuccessCode", "[RestSource][Interface_1_0]") { utility::string_t badManifest = _XPLATSTR( @@ -546,3 +622,24 @@ TEST_CASE("GetManifests_GoodResponse_UnknownInstaller", "[RestSource][Interface_ REQUIRE(manifest.Installers.at(0).BaseInstallerType == InstallerTypeEnum::Unknown); REQUIRE(manifest.Installers.at(0).ProductId.empty()); } + +TEST_CASE("GetManifestByVersion_GoodResponse_MultipleVersions_VersionFound", "[RestSource][Interface_1_0]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, GetManifestsResponse_MultipleVersions()) }; + Interface v1{ TestRestUriString, std::move(helper) }; + + // GetManifests + std::optional<Manifest> manifest = v1.GetManifestByVersion("Foo.Bar", "5.0.0", ""); + REQUIRE(manifest.has_value()); + REQUIRE(manifest->Version == "5.0.0"); +} + +TEST_CASE("GetManifestByVersion_GoodResponse_MultipleVersions_VersionNotFound", "[RestSource][Interface_1_0]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, GetManifestsResponse_MultipleVersions()) }; + Interface v1{ TestRestUriString, std::move(helper) }; + + // GetManifests + std::optional<Manifest> manifest = v1.GetManifestByVersion("Foo.Bar", "7.0.0", ""); + REQUIRE_FALSE(manifest.has_value()); +} diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -528,8 +528,8 @@ namespace AppInstaller::Manifest::YamlParser YamlManifestInfo manifestInfo; YAML::Document doc = YAML::LoadDocument(file.path()); - manifestInfo.Root = std::move(doc).GetRoot(); manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader(); + manifestInfo.Root = std::move(doc).GetRoot(); manifestInfo.FileName = file.path().filename().u8string(); docList.emplace_back(std::move(manifestInfo)); } @@ -538,8 +538,8 @@ namespace AppInstaller::Manifest::YamlParser { YamlManifestInfo manifestInfo; YAML::Document doc = YAML::LoadDocument(inputPath, manifestInfo.StreamSha256); - manifestInfo.Root = std::move(doc).GetRoot(); manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader(); + manifestInfo.Root = std::move(doc).GetRoot(); manifestInfo.FileName = inputPath.filename().u8string(); docList.emplace_back(std::move(manifestInfo)); } @@ -563,8 +563,8 @@ namespace AppInstaller::Manifest::YamlParser { YamlManifestInfo manifestInfo; YAML::Document doc = YAML::LoadDocument(input); - manifestInfo.Root = std::move(doc).GetRoot(); manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader(); + manifestInfo.Root = std::move(doc).GetRoot(); docList.emplace_back(std::move(manifestInfo)); } catch (const std::exception& e) diff --git a/src/Microsoft.Management.Deployment/CatalogPackage.cpp b/src/Microsoft.Management.Deployment/CatalogPackage.cpp @@ -10,6 +10,8 @@ #include "PackageVersionId.h" #include "PackageInstallerInstalledStatus.h" #include "CheckInstalledStatusResult.h" +#include <ComContext.h> +#include <Workflows/ManifestComparator.h> #include <wil\cppwinrt_wrl.h> #include <winget/PinningData.h> #include <winget/PackageVersionSelection.h> @@ -72,20 +74,58 @@ namespace winrt::Microsoft::Management::Deployment::implementation { using namespace AppInstaller::Pinning; + auto availableVersions = AppInstaller::Repository::GetAvailableVersionsForInstalledVersion(m_package); + auto installedVersion = AppInstaller::Repository::GetInstalledVersion(m_package); + PinningData pinningData{ PinningData::Disposition::ReadOnly }; - auto evaluator = pinningData.CreatePinStateEvaluator(PinBehavior::ConsiderPins, GetInstalledVersion(m_package)); + auto evaluator = pinningData.CreatePinStateEvaluator(PinBehavior::ConsiderPins, installedVersion); + + AppInstaller::CLI::Execution::COMContext context; + AppInstaller::Repository::IPackageVersion::Metadata installationMetadata = + installedVersion ? installedVersion->GetMetadata() : AppInstaller::Repository::IPackageVersion::Metadata{}; + AppInstaller::CLI::Workflow::ManifestComparator manifestComparator{ context, installationMetadata }; - std::shared_ptr<::AppInstaller::Repository::IPackageVersion> latestVersion = - evaluator.GetLatestAvailableVersionForPins(::AppInstaller::Repository::GetAvailableVersionsForInstalledVersion(m_package)); - if (latestVersion) + std::shared_ptr<AppInstaller::Repository::IPackageVersion> latestApplicableVersion; + auto availableVersionKeys = availableVersions->GetVersionKeys(); + for (const auto& availableVersionKey : availableVersionKeys) { - m_updateAvailable = evaluator.IsUpdate(latestVersion); + auto availableVersion = availableVersions->GetVersion(availableVersionKey); + + if (installedVersion && !evaluator.IsUpdate(availableVersion)) + { + // Version too low or different channel for upgrade + continue; + } + + if (evaluator.EvaluatePinType(availableVersion) != AppInstaller::Pinning::PinType::Unknown) + { + // Pinned + continue; + } + + auto manifestComparatorResult = manifestComparator.GetPreferredInstaller(availableVersion->GetManifest()); + if (!manifestComparatorResult.installer.has_value()) + { + // No applicable installer + continue; + } + latestApplicableVersion = availableVersion; + if (installedVersion) + { + m_updateAvailable = true; + } + + break; + } + + if (latestApplicableVersion) + { // DefaultInstallVersion hasn't been created yet, create and populate it. - // DefaultInstallVersion is the LatestAvailableVersion of the internal package object. + // DefaultInstallVersion is the latest applicable version of the internal package object. auto latestVersionImpl = winrt::make_self<wil::details::module_count_wrapper< winrt::Microsoft::Management::Deployment::implementation::PackageVersionInfo>>(); - latestVersionImpl->Initialize(std::move(latestVersion)); + latestVersionImpl->Initialize(std::move(latestApplicableVersion)); m_defaultInstallVersion = *latestVersionImpl; } }); diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -476,11 +476,23 @@ namespace winrt::Microsoft::Management::Deployment::implementation } else { - packageVersionInfo = package.DefaultInstallVersion(); + if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::InstallOptions>) + { + packageVersionInfo = package.DefaultInstallVersion(); + } + else if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::DownloadOptions>) + { + // For download, applicability check is not needed. Just use latest. + if (package.AvailableVersions().Size() > 0) + { + packageVersionInfo = package.GetPackageVersionInfo(package.AvailableVersions().GetAt(0)); + } + } } // If the specified version wasn't found then return a failure. This is unusual, since all packages that came from a non-local catalog have a default version, // and the versionId is strongly typed and comes from the CatalogPackage.GetAvailableVersions. - THROW_HR_IF(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND, !packageVersionInfo); + // If version is not specified, DefaultInstallVersion may be empty due to applicability check. + THROW_HR_IF(versionId ? APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND : APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER, !packageVersionInfo); return packageVersionInfo; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1030,8 +1030,10 @@ namespace Microsoft.Management.Deployment { InstallOptions(); - /// Optionally specifies the version from the package to install. If unspecified the version matching - /// CatalogPackage.GetLatestVersion() is used. + /// Optionally specifies the version from the package to install. If unspecified, the CatalogPackage.DefaultInstallVersion + /// version is used. DefaultInstallVersion is the latest applicable version of the package. DefaultInstallVersion may be + /// empty if there's no applicable version. In that case, install attempts without setting this PackageVersionId + /// will return No Applicable Installer error code. PackageVersionId PackageVersionId; /// Specifies alternate location to install package (if supported). @@ -1135,8 +1137,7 @@ namespace Microsoft.Management.Deployment { UninstallOptions(); - /// Optionally specifies the version from the package to uninstall. If unspecified the version matching - /// CatalogPackage.GetLatestVersion() is used. + /// This property is not currently used. The version of CatalogPackage.InstalledVersion is used for uninstall. PackageVersionId PackageVersionId; /// Silent, Interactive, or Default @@ -1232,8 +1233,7 @@ namespace Microsoft.Management.Deployment { RepairOptions(); - /// Optionally specifies the version from the package to repair. If unspecified the version matching - /// CatalogPackage.GetLatestVersion() is used. + /// This property is not currently used. The version of CatalogPackage.InstalledVersion is used for repair. PackageVersionId PackageVersionId; /// The package Repair scope. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/CatalogPackageExtensions.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/CatalogPackageExtensions.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="CatalogPackageExtensions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -36,5 +36,25 @@ namespace Microsoft.WinGet.Client.Engine.Extensions return $"{package.Name} [{package.Id}]"; } } + + /// <summary> + /// Gets the best effort source name of a <see cref="CatalogPackage" /> that matches its Id. + /// This source name is used together with Id in operation output classes for display purposes. + /// </summary> + /// <param name="package">A <see cref="CatalogPackage" /> instance.</param> + /// <returns>The best effort source name of the package.</returns> + public static string? GetSourceName(this CatalogPackage package) + { + for (int i = 0; i < package.AvailableVersions.Count; ++i) + { + var versionInfo = package.GetPackageVersionInfo(package.AvailableVersions[i]); + if (versionInfo.Id == package.Id) + { + return versionInfo.PackageCatalog.Info.Name; + } + } + + return null; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSCatalogPackage.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects using System.Linq; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Client.Engine.Extensions; /// <summary> /// CatalogPackage wrapper object for displaying to PowerShell. @@ -64,7 +65,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { get { - return this.CatalogPackageCOM.DefaultInstallVersion?.PackageCatalog.Info.Name; + return this.CatalogPackageCOM.GetSourceName(); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSDownloadResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSDownloadResult.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSDownloadResult.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Extensions; /// <summary> /// PSDownloadResult. @@ -53,11 +54,11 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the source name of the downloaded package. /// </summary> - public string Source + public string? Source { get { - return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + return this.catalogPackage.GetSourceName(); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSFoundCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSFoundCatalogPackage.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSFoundCatalogPackage.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -25,9 +25,9 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the default install version of the catalog package. /// </summary> - public string Version + public string? Version { - get { return this.CatalogPackageCOM.DefaultInstallVersion.Version; } + get { return this.CatalogPackageCOM.DefaultInstallVersion?.Version; } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSInstallResult.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Extensions; /// <summary> /// PSInstallResult. @@ -53,11 +54,11 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the source name of the installed package. /// </summary> - public string Source + public string? Source { get { - return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + return this.catalogPackage.GetSourceName(); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Extensions; /// <summary> /// PSRepairResult. @@ -53,11 +54,11 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the source name of the repaired package. /// </summary> - public string Source + public string? Source { get { - return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + return this.catalogPackage.GetSourceName(); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSUninstallResult.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Extensions; /// <summary> /// UninstallResult wrapper object for displaying to PowerShell. @@ -53,11 +54,11 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the source name of the uninstalled package. /// </summary> - public string Source + public string? Source { get { - return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + return this.catalogPackage.GetSourceName(); } } diff --git a/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_10ManifestMerged.yaml b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_10ManifestMerged.yaml @@ -260,4 +260,4 @@ Installers: ProductCode: '{Bar}' MSStoreProductIdentifier: fakeIdentifier ManifestType: merged -ManifestVersion: 1.7.0 +ManifestVersion: 1.10.0 diff --git a/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_9ManifestMerged.yaml b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_9ManifestMerged.yaml @@ -250,4 +250,4 @@ Installers: ProductCode: '{Bar}' MSStoreProductIdentifier: fakeIdentifier ManifestType: merged -ManifestVersion: 1.7.0 +ManifestVersion: 1.9.0