Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Consider installer applicability in IsUpdateAvailable COM api #5228

Merged
merged 9 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="UpgradeInterop.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand All @@ -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;
Expand Down Expand Up @@ -230,6 +231,69 @@ public async Task UpgradePortableUninstallPrevious()
searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId);
Assert.AreEqual(searchResult.CatalogPackage.InstalledVersion?.Version, "3.0.0.0");
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 = searchResult.CatalogPackage.AvailableVersions.Single(v => v.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(searchResult.CatalogPackage.InstalledVersion?.Version, "1.0.0.0");

// 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 = searchResult.CatalogPackage.AvailableVersions.Single(v => v.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(searchResult.CatalogPackage.InstalledVersion?.Version, "1.0.0.0");

// There is version 2.0.0.0 in the package available versions.
Assert.True(searchResult.CatalogPackage.AvailableVersions.Any(v => v.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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions src/AppInstallerCLITests/RestInterface_1_0.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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());
}
54 changes: 47 additions & 7 deletions src/Microsoft.Management.Deployment/CatalogPackage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.Management.Deployment/PackageManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation
}
// 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;
}

Expand Down
6 changes: 4 additions & 2 deletions src/Microsoft.Management.Deployment/PackageManager.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,4 @@ Installers:
ProductCode: '{Bar}'
MSStoreProductIdentifier: fakeIdentifier
ManifestType: merged
ManifestVersion: 1.7.0
ManifestVersion: 1.10.0
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,4 @@ Installers:
ProductCode: '{Bar}'
MSStoreProductIdentifier: fakeIdentifier
ManifestType: merged
ManifestVersion: 1.7.0
ManifestVersion: 1.9.0
Loading