Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 40 additions & 0 deletions src/AppInstallerCLITests/CompositeSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,46 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite
REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get() == channel);
}

TEST_CASE("CompositePackage_PrereleaseAvailable_StableInstalled_NoUpdate", "[CompositeSource]")
{
// microsoft/winget-pkgs#368913: a higher-precedence pre-release must not be
// offered as an upgrade to a stable installation.
auto availableVersion = GENERATE("3.14.5"sv, "3.14.5rc1"sv);
const bool isPrerelease = (availableVersion == "3.14.5rc1"sv);

CompositeTestSetup setup;
setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("3.14.4"sv), Criteria());
setup.Available->SearchFunction = [&](const SearchRequest&)
{
auto manifest = MakeDefaultManifest(availableVersion);
// ManifestComparator::GetPreferredInstaller drops Architecture::Unknown on the host.
manifest.Installers[0].BaseInstallerType = Manifest::InstallerTypeEnum::Exe;
manifest.Installers[0].Arch = Utility::Architecture::Neutral;

SearchResult result;
result.Matches.emplace_back(
TestCompositePackage::Make(std::vector<Manifest::Manifest>{ manifest }, setup.Available),
Criteria());
return result;
};

SearchResult result = setup.Search();
REQUIRE(result.Matches.size() == 1);

auto latest = GetLatestApplicableVersion(result.Matches[0].Package);
if (isPrerelease)
{
REQUIRE_FALSE(latest.UpdateAvailable);
REQUIRE_FALSE(latest.LatestApplicableVersion);
}
else
{
REQUIRE(latest.UpdateAvailable);
REQUIRE(latest.LatestApplicableVersion);
REQUIRE(latest.LatestApplicableVersion->GetProperty(PackageVersionProperty::Version).get() == availableVersion);
}
}

TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource]")
{
TestCommon::TestUserSettings testSettings;
Expand Down
24 changes: 24 additions & 0 deletions src/AppInstallerRepositoryCore/PackageVersionSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ namespace AppInstaller::Repository
{
namespace
{
// Heuristic: any non-numeric tail in any Part. Catches both 1.0.0-rc.1 (SemVer)
// and 1.0.0rc1 (e.g. Python PEP 440); the manifest schema has no IsPrerelease field.
bool LooksLikePrerelease(const Utility::Version& version)
{
for (const auto& part : version.GetParts())
{
if (!part.Other.empty())
{
return true;
}
}
return false;
}

std::shared_ptr<IPackage> GetAvailablePackageFromSource(const std::vector<std::shared_ptr<IPackage>>& packages, const std::string_view sourceIdentifier)
{
for (const std::shared_ptr<IPackage>& package : packages)
Expand Down Expand Up @@ -175,6 +189,9 @@ namespace AppInstaller::Repository
}
AppInstaller::Manifest::ManifestComparator manifestComparator{ options };

const bool installedIsPrerelease = installedVersion &&
LooksLikePrerelease(Utility::Version{ installedVersion->GetProperty(PackageVersionProperty::Version) });

auto availableVersionKeys = availableVersions->GetVersionKeys();
for (const auto& availableVersionKey : availableVersionKeys)
{
Expand All @@ -186,6 +203,13 @@ namespace AppInstaller::Repository
continue;
}

if (installedVersion && !installedIsPrerelease &&
LooksLikePrerelease(Utility::Version{ availableVersion->GetProperty(PackageVersionProperty::Version) }))
{
// Stable installations are not auto-upgraded to a pre-release
continue;
}

if (evaluator.EvaluatePinType(availableVersion) != AppInstaller::Pinning::PinType::Unknown)
{
// Pinned
Expand Down