winget-cli

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

commit b21389cbf07126f7678cee953543f5e0bcb0e0c1
parent 78aca6934ed452dbbbb7a2c32bc1c87ea0296740
Author: Flor Chacón <14323496+florelis@users.noreply.github.com>
Date:   Wed,  8 Feb 2023 16:11:25 -0800

Implement package pinning (#2813)


Diffstat:
Msrc/AppInstallerCLICore/Argument.cpp | 4+++-
Msrc/AppInstallerCLICore/Commands/ShowCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 6++++++
Msrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp | 13++++++++++++-
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 4++--
Msrc/AppInstallerCLICore/Workflows/PinFlow.cpp | 175++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/AppInstallerCLICore/Workflows/PinFlow.h | 4++--
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 68++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.h | 6+++---
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 132++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 36+++++++++++++++++++++++++++---------
Msrc/AppInstallerCLIE2ETests/Constants.cs | 1+
Asrc/AppInstallerCLIE2ETests/Pinning.cs | 246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 15++++++++++++++-
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml | 20++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml | 20++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml | 4++--
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe_InvalidRelativeFilePath.yaml | 4+++-
Msrc/AppInstallerCLIE2ETests/TestIndexSetup.cs | 18++++++++++++++++--
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 54++++++++++++++++++++++++++++++++++--------------------
Msrc/AppInstallerCLITests/CompositeSource.cpp | 415+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/AppInstallerCLITests/PackageTrackingCatalog.cpp | 10+++++-----
Msrc/AppInstallerCLITests/PinFlow.cpp | 10++++++++++
Msrc/AppInstallerCLITests/PinningIndex.cpp | 4++--
Msrc/AppInstallerCLITests/SQLiteIndexSource.cpp | 4++--
Msrc/AppInstallerCLITests/TestHooks.h | 18++++++++++++++++++
Msrc/AppInstallerCLITests/TestSource.cpp | 6+++---
Msrc/AppInstallerCLITests/TestSource.h | 4++--
Msrc/AppInstallerCLITests/UpdateFlow.cpp | 10+++++-----
Msrc/AppInstallerCLITests/Versions.cpp | 19+++++++++++++++++++
Msrc/AppInstallerCommonCore/Pin.cpp | 41+++++++++++++++++++++--------------------
Msrc/AppInstallerCommonCore/Public/AppInstallerVersions.h | 21++++++++++++++-------
Msrc/AppInstallerCommonCore/Public/winget/Pin.h | 21+++++++++++++++------
Msrc/AppInstallerCommonCore/Versions.cpp | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/CompositeSource.cpp | 412++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp | 31++++++++++++++++++++++++++++++-
Msrc/AppInstallerRepositoryCore/Microsoft/PinningIndex.h | 6+++++-
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp | 8++++----
Msrc/AppInstallerRepositoryCore/PackageInstalledStatus.cpp | 2+-
Msrc/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp | 3++-
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h | 58++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/AppInstallerRepositoryCore/RepositorySearch.cpp | 24------------------------
Msrc/AppInstallerRepositoryCore/Rest/RestSource.cpp | 4++--
Msrc/AppInstallerSharedLib/Errors.cpp | 2++
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 1+
Msrc/Microsoft.Management.Deployment/CatalogPackage.cpp | 4++--
48 files changed, 1651 insertions(+), 364 deletions(-)

diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -149,8 +149,10 @@ namespace AppInstaller::CLI return { type, "all"_liv, 'r', "recurse"_liv, ArgTypeCategory::MultiplePackages }; case Execution::Args::Type::IncludeUnknown: return { type, "include-unknown"_liv, 'u', "unknown"_liv }; + case Execution::Args::Type::IncludePinned: + return { type, "include-pinned"_liv, "pinned"_liv, ArgTypeCategory::CopyFlagToSubContext }; case Execution::Args::Type::UninstallPrevious: - return { type, "uninstall-previous"_liv, ArgTypeCategory::InstallerBehavior }; + return { type, "uninstall-previous"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext }; // Show command case Execution::Args::Type::ListVersions: diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -86,7 +86,7 @@ namespace AppInstaller::CLI else { context << - Workflow::GetManifest << + Workflow::GetManifest(/* considerPins */ false) << Workflow::ReportManifestIdentity << Workflow::SelectInstaller << Workflow::ShowManifestInfo; diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -65,6 +65,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::CustomHeader), Argument{ Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, Argument{ Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag }, + Argument{ Args::Type::IncludePinned, Resource::String::IncludePinnedArgumentDescription, ArgumentType::Flag}, Argument::ForType(Args::Type::UninstallPrevious), Argument::ForType(Args::Type::Force), }; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -86,6 +86,7 @@ namespace AppInstaller::CLI::Execution // Upgrade command All, // Used in Update command to update all installed packages to latest IncludeUnknown, // Used in Upgrade command to allow upgrades of packages with unknown versions + IncludePinned, // Used in Upgrade command to allow upgrades to pinned packages (only for pinning type of pins) UninstallPrevious, // Used in Upgrade command to override the default manifest behavior to UninstallPrevious // Show command diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -121,6 +121,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled); + WINGET_DEFINE_RESOURCE_STRINGID(IncludePinnedArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(IncompatibleArgumentsProvided); WINGET_DEFINE_RESOURCE_STRINGID(InstallAndUpgradeCommandsReportDependencies); @@ -245,6 +246,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsPrompt); WINGET_DEFINE_RESOURCE_STRINGID(PackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(PackageDependencies); + WINGET_DEFINE_RESOURCE_STRINGID(PackageIsPinned); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); WINGET_DEFINE_RESOURCE_STRINGID(PinAddBlockingArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandLongDescription); @@ -262,6 +264,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinRemovedSuccessfully); WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful); @@ -433,10 +436,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeAvailableForPinned); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeBlockingPinCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnology); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnologyInNewerVersions); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeIsPinned); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradePinnedByUserCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeRequireExplicitCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionExplanation); diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -41,7 +41,18 @@ namespace AppInstaller::CLI::Workflow const auto& package = match.Package; auto packageId = package->GetProperty(PackageProperty::Id); m_nodePackageInstalledVersion = package->GetInstalledVersion(); - m_nodePackageLatestVersion = package->GetLatestAvailableVersion(); + + PinBehavior pinBehavior; + if (m_context.Args.Contains(Execution::Args::Type::Force)) + { + pinBehavior = PinBehavior::IgnorePins; + } + else + { + pinBehavior = m_context.Args.Contains(Execution::Args::Type::IncludePinned) ? PinBehavior::IncludePinned : PinBehavior::ConsiderPins; + } + + m_nodePackageLatestVersion = package->GetLatestAvailableVersion(pinBehavior); if (m_nodePackageInstalledVersion && dependencyNode.IsVersionOk(Utility::Version(m_nodePackageInstalledVersion->GetProperty(PackageVersionProperty::Version)))) { diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -65,13 +65,13 @@ namespace AppInstaller::CLI::Workflow { if (!checkVersion) { - return package->GetLatestAvailableVersion(); + return package->GetLatestAvailableVersion(PinBehavior::IgnorePins); } auto availablePackageVersion = package->GetAvailableVersion({ "", version, channel }); if (!availablePackageVersion) { - availablePackageVersion = package->GetLatestAvailableVersion(); + availablePackageVersion = package->GetLatestAvailableVersion(PinBehavior::IgnorePins); if (availablePackageVersion) { // Warn installed version is not available. diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -57,17 +57,27 @@ namespace AppInstaller::CLI::Workflow { auto package = context.Get<Execution::Data::Package>(); std::vector<Pinning::Pin> pins; - - // TODO: We should support querying the multiple sources for a package, instead of just one - auto availableVersion = package->GetLatestAvailableVersion(); + std::set<std::string> sources; auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); - auto pin = pinningIndex->GetPin({ - availableVersion->GetProperty(PackageVersionProperty::Id).get(), - availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }); - if (pin) + + auto packageVersionKeys = package->GetAvailableVersionKeys(); + for (const auto& versionKey : packageVersionKeys) + { - pins.emplace_back(std::move(pin.value())); + auto availableVersion = package->GetAvailableVersion(versionKey); + Pinning::PinKey pinKey{ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; + + if (sources.insert(pinKey.SourceId).second) + { + auto pin = pinningIndex->GetPin(pinKey); + if (pin) + { + pins.emplace_back(std::move(pin.value())); + } + } } context.Add<Execution::Data::Pins>(std::move(pins)); @@ -78,49 +88,73 @@ namespace AppInstaller::CLI::Workflow auto package = context.Get<Execution::Data::Package>(); auto installedVersion = context.Get<Execution::Data::InstalledPackageVersion>(); - std::vector<Pinning::Pin> pins; - - // TODO: We should support querying the multiple sources for a package, instead of just one - auto availableVersion = package->GetLatestAvailableVersion(); - - Pinning::PinKey pinKey{ - availableVersion->GetProperty(PackageVersionProperty::Id).get(), - availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; - auto pin = CreatePin(context, pinKey.PackageId, pinKey.SourceId); - AICLI_LOG(CLI, Info, << "Adding pin with type " << ToString(pin.GetType()) << " for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "]"); + auto installedVersionString = installedVersion->GetProperty(PackageVersionProperty::Version); + std::vector<Pinning::Pin> pinsToAddOrUpdate; + std::set<std::string> sources; auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); - auto existingPin = pinningIndex->GetPin(pinKey); - if (existingPin) + auto packageVersionKeys = package->GetAvailableVersionKeys(); + for (const auto& versionKey : packageVersionKeys) + { - // Pin already exists. - // If it is the same, we do nothing. If it is different, check for the --force arg - if (pin == existingPin) + auto availableVersion = package->GetAvailableVersion(versionKey); + Pinning::PinKey pinKey{ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; + + if (!sources.insert(pinKey.SourceId).second) { - AICLI_LOG(CLI, Info, << "Pin already exists"); - context.Reporter.Info() << Resource::String::PinAlreadyExists << std::endl; - return; + // We already considered the pin for this source + continue; } - AICLI_LOG(CLI, Info, << "Another pin already exists for the package"); - if (context.Args.Contains(Execution::Args::Type::Force)) + auto pin = CreatePin(context, pinKey.PackageId, pinKey.SourceId); + AICLI_LOG(CLI, Info, << "Evaluating pin with type " << ToString(pin.GetType()) << " for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "]"); + + auto existingPin = pinningIndex->GetPin(pinKey); + + if (existingPin) { - AICLI_LOG(CLI, Info, << "Overwriting pin due to --force argument"); - context.Reporter.Warn() << Resource::String::PinExistsOverwriting << std::endl; - pinningIndex->UpdatePin(pin); + auto packageName = availableVersion->GetProperty(PackageVersionProperty::Name); + + // Pin already exists. + // If it is the same, we do nothing. If it is different, check for the --force arg + if (pin == existingPin) + { + AICLI_LOG(CLI, Info, << "Pin already exists"); + context.Reporter.Info() << Resource::String::PinAlreadyExists(packageName) << std::endl; + continue; + } + + AICLI_LOG(CLI, Info, << "Another pin already exists for the package for source " << pinKey.SourceId); + if (context.Args.Contains(Execution::Args::Type::Force)) + { + AICLI_LOG(CLI, Info, << "Overwriting pin due to --force argument"); + context.Reporter.Warn() << Resource::String::PinExistsOverwriting(packageName) << std::endl; + pinsToAddOrUpdate.push_back(std::move(pin)); + } + else + { + context.Reporter.Error() << Resource::String::PinExistsUseForceArg(packageName) << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS); + } } else { - context.Reporter.Error() << Resource::String::PinExistsUseForceArg << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS); + pinsToAddOrUpdate.push_back(std::move(pin)); } } - else + + if (!pinsToAddOrUpdate.empty()) { - pinningIndex->AddPin(pin); - AICLI_LOG(CLI, Info, << "Finished adding pin"); + for (const auto& pin : pinsToAddOrUpdate) + + { + pinningIndex->AddOrUpdatePin(pin); + } + context.Reporter.Info() << Resource::String::PinAdded << std::endl; } } @@ -129,23 +163,42 @@ namespace AppInstaller::CLI::Workflow { auto package = context.Get<Execution::Data::Package>(); std::vector<Pinning::Pin> pins; - - // TODO: We should support querying the multiple sources for a package, instead of just one - auto availableVersion = package->GetLatestAvailableVersion(); + std::set<std::string> sources; auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); - Pinning::PinKey pinKey{ - availableVersion->GetProperty(PackageVersionProperty::Id).get(), - availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; - AICLI_LOG(CLI, Info, << "Removing pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); - if (!pinningIndex->GetPin(pinKey)) + bool pinExists = false; + + // Note that if a source was specified in the command line, + // that will be the only one we get version keys from. + // So, we remove pins from all sources unless one was provided. + auto packageVersionKeys = package->GetAvailableVersionKeys(); + for (const auto& versionKey : packageVersionKeys) + + { + auto availableVersion = package->GetAvailableVersion(versionKey); + Pinning::PinKey pinKey{ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; + + if (sources.insert(pinKey.SourceId).second) + { + if (pinningIndex->GetPin(pinKey)) + { + AICLI_LOG(CLI, Info, << "Removing pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + pinningIndex->RemovePin(pinKey); + pinExists = true; + } + } + } + + if (!pinExists) { AICLI_LOG(CLI, Warning, << "Pin does not exist"); - context.Reporter.Warn() << Resource::String::PinDoesNotExist(pinKey.PackageId) << std::endl; + context.Reporter.Warn() << Resource::String::PinDoesNotExist(package->GetProperty(PackageProperty::Name)) << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); } - pinningIndex->RemovePin(pinKey); + context.Reporter.Info() << Resource::String::PinRemovedSuccessfully << std::endl; } void ReportPins(Execution::Context& context) @@ -157,7 +210,13 @@ namespace AppInstaller::CLI::Workflow return; } - // TODO: Use package and source names + // Get a mapping of source IDs to names so that we can show something nicer + std::map<std::string, std::string> sourceNames; + for (const auto& source : Repository::Source::GetCurrentSources()) + { + sourceNames[source.Identifier] = source.Name; + } + Execution::TableOutput<4> table(context.Reporter, { Resource::String::SearchId, @@ -168,12 +227,11 @@ namespace AppInstaller::CLI::Workflow for (const auto& pin : pins) { - // TODO: Avoid these conversions to string table.OutputLine({ pin.GetPackageId(), - std::string{ pin.GetSourceId() }, - pin.GetGatedVersion().ToString(), + sourceNames[pin.GetSourceId()], std::string{ ToString(pin.GetType()) }, + pin.GetGatedVersion().ToString(), }); } @@ -216,14 +274,23 @@ namespace AppInstaller::CLI::Workflow const auto& source = context.Get<Execution::Data::Source>(); std::vector<Pinning::Pin> matchingPins; - std::copy_if(pins.begin(), pins.end(), std::back_inserter(matchingPins), [&](Pinning::Pin pin) { - // TODO: Filter to source + for (const auto& pin : pins) + + { SearchRequest searchRequest; searchRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, pin.GetPackageId()); auto searchResult = source.Search(searchRequest); - return !searchResult.Matches.empty(); - }); + // Ensure the match comes from the right source + for (const auto& match : searchResult.Matches) + { + auto availableVersion = match.Package->GetAvailableVersion({ pin.GetSourceId(), "", "" }); + if (availableVersion) + { + matchingPins.push_back(pin); + } + } + } context.Add<Execution::Data::Pins>(std::move(matchingPins)); } diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h @@ -29,7 +29,7 @@ namespace AppInstaller::CLI::Workflow // There may be several if a package is available from multiple sources. // Required Args: None // Inputs: PinningIndex, Package - // Outputs Pins + // Outputs: Pins void SearchPin(Execution::Context& context); // Adds a pin for the current package. @@ -57,7 +57,7 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ResetAllPins(Execution::Context& context); - // Updates the list of pins to include only those matching the current open source + // Updates the list of pins to include only those matching the current open source. // Required Args: None // Inputs: Pins, Source // Outputs: None diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -7,8 +7,11 @@ #include "InstallFlow.h" #include "UpdateFlow.h" #include "ManifestComparator.h" +#include <Microsoft/PinningIndex.h> using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Pinning; namespace AppInstaller::CLI::Workflow { @@ -39,6 +42,7 @@ namespace AppInstaller::CLI::Workflow { auto package = context.Get<Execution::Data::Package>(); auto installedPackage = context.Get<Execution::Data::InstalledPackageVersion>(); + const bool reportVersionNotFound = m_isSinglePackage; bool isUpgrade = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate);; Utility::Version installedVersion; @@ -50,11 +54,12 @@ namespace AppInstaller::CLI::Workflow ManifestComparator manifestComparator(context, isUpgrade ? installedPackage->GetMetadata() : IPackageVersion::Metadata{}); bool versionFound = false; bool installedTypeInapplicable = false; + bool packagePinned = false; if (isUpgrade && installedVersion.IsUnknown() && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) { - // the package has an unknown version and the user did not request to upgrade it anyway. - if (m_reportVersionNotFound) + // the package has an unknown version and the user did not request to upgrade it anyway + if (reportVersionNotFound) { context.Reporter.Info() << Resource::String::UpgradeUnknownVersionExplanation << std::endl; } @@ -62,6 +67,10 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } + // If we are updating a single package or we got the --include-pinned flag, + // we include packages with Pinning pins + const bool includePinned = m_isSinglePackage || context.Args.Contains(Execution::Args::Type::IncludePinned); + // The version keys should have already been sorted by version const auto& versionKeys = package->GetAvailableVersionKeys(); for (const auto& key : versionKeys) @@ -69,6 +78,23 @@ namespace AppInstaller::CLI::Workflow // Check Applicable Version if (!isUpgrade || IsUpdateVersionApplicable(installedVersion, Utility::Version(key.Version))) { + // Check if the package is pinned + if (key.PinnedState == Pinning::PinType::Blocking || + key.PinnedState == Pinning::PinType::Gating || + (key.PinnedState == Pinning::PinType::Pinning && !includePinned)) + { + AICLI_LOG(CLI, Info, << "Package [" << package->GetProperty(PackageProperty::Id) << " with Version[" << key.Version << "] from Source[" << key.SourceId << "] has a Pin with type[" << ToString(key.PinnedState) << "]"); + if (context.Args.Contains(Execution::Args::Type::Force)) + { + AICLI_LOG(CLI, Info, << "Ignoring pin due to --force argument"); + } + else + { + packagePinned = true; + continue; + } + } + auto packageVersion = package->GetAvailableVersion(key); auto manifest = packageVersion->GetManifest(); @@ -116,12 +142,17 @@ namespace AppInstaller::CLI::Workflow if (!versionFound) { - if (m_reportVersionNotFound) + if (reportVersionNotFound) { if (installedTypeInapplicable) { context.Reporter.Info() << Resource::String::UpgradeDifferentInstallTechnologyInNewerVersions << std::endl; } + else if (packagePinned) + { + context.Reporter.Info() << Resource::String::UpgradeIsPinned << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED); + } else if (isUpgrade) { context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; @@ -190,19 +221,18 @@ namespace AppInstaller::CLI::Workflow continue; } - // Filter out packages that require explicit upgrades. - // We require explicit upgrades only if the installed version is pinned, - // either because it was manually pinned or because the manifest indicated - // RequireExplicitUpgrade. - // Note that this does not consider whether the update to be installed has - // RequireExplicitUpgrade. While this has the downside of not working with - // packages installed from another source, it ensures consistency with the - // list of available updates (there we don't have the selected installer) - // and at most we will update each package like this once. + // Filter out packages that require explicit upgrade. + // User-defined pins are handled when selecting the version to use. auto installedMetadata = updateContext.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); - auto pinnedState = ConvertToPackagePinnedStateEnum(installedMetadata[PackageVersionMetadata::PinnedState]); - if (pinnedState != PackagePinnedState::NotPinned) + auto pinnedState = ConvertToPinTypeEnum(installedMetadata[PackageVersionMetadata::PinnedState]); + if (pinnedState == PinType::PinnedByManifest) { + // Note that for packages pinned by the manifest + // this does not consider whether the update to be installed has + // RequireExplicitUpgrade. While this has the downside of not working with + // packages installed from another source, it ensures consistency with the + // list of available updates (there we don't have the selected installer) + // and at most we will update each package like this once. AICLI_LOG(CLI, Info, << "Skipping " << match.Package->GetProperty(PackageProperty::Id) << " as it requires explicit upgrade"); ++packagesThatRequireExplicitSkipped; continue; @@ -227,13 +257,13 @@ namespace AppInstaller::CLI::Workflow if (packagesWithUnknownVersionSkipped > 0) { AICLI_LOG(CLI, Info, << packagesWithUnknownVersionSkipped << " package(s) skipped due to unknown installed version"); - context.Reporter.Info() << packagesWithUnknownVersionSkipped << " " << Resource::String::UpgradeUnknownVersionCount << std::endl; + context.Reporter.Info() << Resource::String::UpgradeUnknownVersionCount(packagesWithUnknownVersionSkipped) << std::endl; } if (packagesThatRequireExplicitSkipped > 0) { AICLI_LOG(CLI, Info, << packagesThatRequireExplicitSkipped << " package(s) skipped due to requiring explicit upgrade"); - context.Reporter.Info() << packagesThatRequireExplicitSkipped << " " << Resource::String::UpgradeRequireExplicitCount << std::endl; + context.Reporter.Info() << Resource::String::UpgradeRequireExplicitCount(packagesThatRequireExplicitSkipped) << std::endl; } } @@ -264,16 +294,14 @@ namespace AppInstaller::CLI::Workflow if (context.Args.Contains(Execution::Args::Type::Version)) { // If version specified, use the version and verify applicability - context << - GetManifestFromPackage; + context << GetManifestFromPackage(/* considerPins */ true); if (m_isUpgrade) { context << EnsureUpdateVersionApplicable; } - context << - SelectInstaller; + context << SelectInstaller; } else { diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.h b/src/AppInstallerCLICore/Workflows/UpdateFlow.h @@ -12,13 +12,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: Manifest?, Installer? struct SelectLatestApplicableVersion : public WorkflowTask { - SelectLatestApplicableVersion(bool reportVersionNotFound) : - WorkflowTask("SelectLatestApplicableUpdate"), m_reportVersionNotFound(reportVersionNotFound) {} + SelectLatestApplicableVersion(bool isSinglePackage) : + WorkflowTask("SelectLatestApplicableUpdate"), m_isSinglePackage(isSinglePackage) {} void operator()(Execution::Context& context) const override; private: - bool m_reportVersionNotFound; + bool m_isSinglePackage; }; // Ensures the update package has higher version than installed diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -6,15 +6,18 @@ #include "ManifestComparator.h" #include "PromptFlow.h" #include "TableOutput.h" +#include <winget/ExperimentalFeature.h> #include <winget/ManifestYamlParser.h> +#include <winget/Pin.h> +using namespace std::string_literals; +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Pinning; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Settings; namespace AppInstaller::CLI::Workflow { - using namespace std::string_literals; - using namespace AppInstaller::Utility::literals; - using namespace AppInstaller::Repository; - namespace { std::string GetMatchCriteriaDescriptor(const ResultMatch& match) @@ -574,7 +577,7 @@ namespace AppInstaller::CLI::Workflow for (size_t i = 0; i < searchResult.Matches.size(); ++i) { - auto latestVersion = searchResult.Matches[i].Package->GetLatestAvailableVersion(); + auto latestVersion = searchResult.Matches[i].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); table.OutputLine({ latestVersion->GetProperty(PackageVersionProperty::Name), @@ -685,7 +688,7 @@ namespace AppInstaller::CLI::Workflow auto package = searchResult.Matches[i].Package; std::string sourceName; - auto latest = package->GetLatestAvailableVersion(); + auto latest = package->GetLatestAvailableVersion(PinBehavior::IgnorePins); if (latest) { auto source = latest->GetSource(); @@ -719,17 +722,30 @@ namespace AppInstaller::CLI::Workflow int availableUpgradesCount = 0; int packagesWithUnknownVersionSkipped = 0; + int packagesWithUserPinsSkipped = 0; auto &source = context.Get<Execution::Data::Source>(); bool shouldShowSource = source.IsComposite() && source.GetAvailableSources().size() > 1; + PinBehavior pinBehavior; + if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::Force)) + { + // For listing upgrades, show the version we would upgrade to with the given pins. + pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) ? PinBehavior::IncludePinned : PinBehavior::ConsiderPins; + } + else + { + // For listing installed apps or if we are ignoring pins due to --force, show the latest available. + pinBehavior = PinBehavior::IgnorePins; + } + for (const auto& match : searchResult.Matches) { auto installedVersion = match.Package->GetInstalledVersion(); if (installedVersion) { - auto latestVersion = match.Package->GetLatestAvailableVersion(); - bool updateAvailable = match.Package->IsUpdateAvailable(); + auto latestVersion = match.Package->GetLatestAvailableVersion(pinBehavior); + bool updateAvailable = match.Package->IsUpdateAvailable(pinBehavior); if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown() && updateAvailable) { @@ -738,6 +754,16 @@ namespace AppInstaller::CLI::Workflow continue; } + if (m_onlyShowUpgrades && !updateAvailable && ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning)) + { + bool updateAvailableWithoutPins = match.Package->IsUpdateAvailable(PinBehavior::IgnorePins); + if (updateAvailableWithoutPins) + { + ++packagesWithUserPinsSkipped; + continue; + } + } + // The only time we don't want to output a line is when filtering and no update is available. if (updateAvailable || !m_onlyShowUpgrades) { @@ -766,9 +792,8 @@ namespace AppInstaller::CLI::Workflow shouldShowSource ? sourceName : Utility::LocIndString() ); - auto pinnedState = ConvertToPackagePinnedStateEnum(installedVersion->GetMetadata()[PackageVersionMetadata::PinnedState]); - bool requiresExplicitUpgrade = m_onlyShowUpgrades && pinnedState != PackagePinnedState::NotPinned; - if (requiresExplicitUpgrade) + auto pinnedState = ConvertToPinTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::PinnedState]); + if (m_onlyShowUpgrades && pinnedState == PinType::PinnedByManifest) { linesForExplicitUpgrade.push_back(std::move(line)); } @@ -810,7 +835,13 @@ namespace AppInstaller::CLI::Workflow if (packagesWithUnknownVersionSkipped > 0) { AICLI_LOG(CLI, Info, << packagesWithUnknownVersionSkipped << " package(s) skipped due to unknown installed version"); - context.Reporter.Info() << packagesWithUnknownVersionSkipped << " " << Resource::String::UpgradeUnknownVersionCount << std::endl; + context.Reporter.Info() << Resource::String::UpgradeUnknownVersionCount(packagesWithUnknownVersionSkipped) << std::endl; + } + + if (packagesWithUserPinsSkipped > 0) + { + AICLI_LOG(CLI, Info, << packagesWithUserPinsSkipped << " package(s) skipped due to user pins"); + context.Reporter.Info() << Resource::String::UpgradePinnedByUserCount(packagesWithUserPinsSkipped) << std::endl; } } } @@ -875,7 +906,71 @@ namespace AppInstaller::CLI::Workflow void GetManifestWithVersionFromPackage::operator()(Execution::Context& context) const { PackageVersionKey key("", m_version, m_channel); - auto requestedVersion = context.Get<Execution::Data::Package>()->GetAvailableVersion(key); + + std::shared_ptr<IPackage> package = context.Get<Execution::Data::Package>(); + std::shared_ptr<IPackageVersion> requestedVersion; + + if (m_considerPins && ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning)) + { + bool isPinned = false; + + // TODO: The logic here will probably have to get more difficult once we support channels + if (Utility::IsEmptyOrWhitespace(m_version) && Utility::IsEmptyOrWhitespace(m_channel)) + { + PinBehavior pinBehavior; + if (context.Args.Contains(Execution::Args::Type::Force)) + { + // --force ignores any pins + pinBehavior = PinBehavior::IgnorePins; + } + else + { + pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) ? PinBehavior::IncludePinned : PinBehavior::ConsiderPins; + } + + requestedVersion = package->GetLatestAvailableVersion(pinBehavior); + + if (!requestedVersion) + { + // Check whether we didn't find the latest version because it was pinned or because there wasn't one + auto latestVersion = package->GetLatestAvailableVersion(PinBehavior::IgnorePins); + if (latestVersion) + { + isPinned = true; + } + } + } + else + { + auto requestedVersionAndPin = package->GetAvailableVersionAndPin(key); + requestedVersion = requestedVersionAndPin.first; + auto pin = requestedVersionAndPin.second; + + isPinned = + pin == Pinning::PinType::Blocking || + pin == Pinning::PinType::Gating || + (pin == Pinning::PinType::Pinning && !context.Args.Contains(Execution::Args::Type::IncludePinned)); + } + + if (isPinned) + { + if (context.Args.Contains(Execution::Args::Type::Force)) + { + AICLI_LOG(CLI, Info, << "Ignoring pin on package due to --force argument"); + } + else + { + AICLI_LOG(CLI, Error, << "The requested package version is unavailable because of a pin"); + context.Reporter.Error() << Resource::String::PackageIsPinned << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED); + } + } + } + else + { + // The simple case: Just look up the requested version + requestedVersion = package->GetAvailableVersion(key); + } std::optional<Manifest::Manifest> manifest; if (requestedVersion) @@ -912,9 +1007,12 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::PackageVersion>(std::move(requestedVersion)); } - void GetManifestFromPackage(Execution::Context& context) + void GetManifestFromPackage::operator()(Execution::Context& context) const { - context << GetManifestWithVersionFromPackage(context.Args.GetArg(Execution::Args::Type::Version), context.Args.GetArg(Execution::Args::Type::Channel)); + context << GetManifestWithVersionFromPackage( + context.Args.GetArg(Execution::Args::Type::Version), + context.Args.GetArg(Execution::Args::Type::Channel), + m_considerPins); } void VerifyFile::operator()(Execution::Context& context) const @@ -985,7 +1083,7 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id, manifest.Version, m_level); } - void GetManifest(Execution::Context& context) + void GetManifest::operator()(Execution::Context& context) const { if (context.Args.Contains(Execution::Args::Type::Manifest)) { @@ -999,7 +1097,7 @@ namespace AppInstaller::CLI::Workflow SearchSourceForSingle << HandleSearchResultFailures << EnsureOneMatchFromSearchResult(false) << - GetManifestFromPackage; + GetManifestFromPackage(m_considerPins); } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -241,29 +241,38 @@ namespace AppInstaller::CLI::Workflow }; // Gets the manifest from package. - // Required Args: Version and channel; can be empty + // Required Args: Version and channel; can be empty. A flag indicating whether to consider package pins // Inputs: Package // Outputs: Manifest, PackageVersion struct GetManifestWithVersionFromPackage : public WorkflowTask { - GetManifestWithVersionFromPackage(const Utility::VersionAndChannel& versionAndChannel) : - WorkflowTask("GetManifestWithVersionFromPackage"), m_version(versionAndChannel.GetVersion().ToString()), m_channel(versionAndChannel.GetChannel().ToString()) {} + GetManifestWithVersionFromPackage(std::string_view version, std::string_view channel, bool considerPins) : + WorkflowTask("GetManifestWithVersionFromPackage"), m_version(version), m_channel(channel), m_considerPins(considerPins) {} - GetManifestWithVersionFromPackage(std::string_view version, std::string_view channel) : - WorkflowTask("GetManifestWithVersionFromPackage"), m_version(version), m_channel(channel) {} + GetManifestWithVersionFromPackage(const Utility::VersionAndChannel& versionAndChannel, bool considerPins) : + GetManifestWithVersionFromPackage(versionAndChannel.GetVersion().ToString(), versionAndChannel.GetChannel().ToString(), considerPins) {} void operator()(Execution::Context& context) const override; private: std::string_view m_version; std::string_view m_channel; + bool m_considerPins; }; // Gets the manifest from package. - // Required Args: None - // Inputs: Package + // Required Args: A value indicating whether to consider pins + // Inputs: Package. Optionally Version and Channel // Outputs: Manifest, PackageVersion - void GetManifestFromPackage(Execution::Context& context); + struct GetManifestFromPackage : public WorkflowTask + { + GetManifestFromPackage(bool considerPins) : WorkflowTask("GetManifestFromPackage"), m_considerPins(considerPins) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_considerPins; + }; // Ensures the file exists and is not a directory. // Required Args: the one given @@ -334,7 +343,16 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: None // Outputs: Manifest - void GetManifest(Execution::Context& context); + struct GetManifest : public WorkflowTask + { + GetManifest(bool considerPins) : WorkflowTask("GetManifest"), m_considerPins(considerPins) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_considerPins; + }; + // Selects the installer from the manifest, if one is applicable. // Required Args: None diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -230,6 +230,7 @@ namespace AppInstallerCLIE2ETests public const int ERROR_MULTIPLE_INSTALL_FAILED = unchecked((int)0x8A150065); public const int ERROR_MULTIPLE_UNINSTALL_FAILED = unchecked((int)0x8A150066); public const int ERROR_NOT_ALL_QUERIES_FOUND_SINGLE = unchecked((int)0x8A150067); + public const int ERROR_PACKAGE_IS_PINNED = unchecked((int)0x8A150068); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIE2ETests/Pinning.cs b/src/AppInstallerCLIE2ETests/Pinning.cs @@ -0,0 +1,246 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Pinning.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using NUnit.Framework; + using static AppInstallerCLIE2ETests.TestCommon; + + /// <summary> + /// Test upgrading pinned packages. + /// </summary> + public class Pinning : BaseCommand + { + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("pinning", true); + } + + /// <summary> + /// Set up for all tests. + /// </summary> + [SetUp] + public void Setup() + { + // All tests use TestExeInstaller; try to clean it up for failure cases, + // then install the base version for pinning + TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestExeInstaller"); + TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller -v 1.0.0.0"); + TestCommon.RunAICLICommand("pin remove", "AppInstallerTest.TestExeInstaller"); + } + + /// <summary> + /// Clean up done after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTearDown() + { + TestCommon.RunAICLICommand("pin remove", "AppInstallerTest.TestExeInstaller"); + TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestExeInstaller"); + } + + // All tests do roughly the same with different types of pins: + // * Check that the available version shown by list is the latest + // * Check that the available version shown by upgrade is appropriate for the pin, + // including checks with flags to include pinned. + // * Check that an upgrade installs the right version + + /// <summary> + /// Tests upgrading a package when there are no pins on it. + /// </summary> + [Test] + public void UpgradeWithNoPins() + { + RunCommandResult result; + + result = TestCommon.RunAICLICommand("list", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "List shows the latest available version"); + + result = TestCommon.RunAICLICommand("upgrade", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "The latest upgrade-able version is the same if there are no pins"); + } + + /// <summary> + /// Tests upgrading a package when it has a pinning pin. + /// </summary> + [Test] + public void UpgradeWithPinningPin() + { + RunCommandResult result; + string installDir = Path.GetTempPath(); + + // The base version of this app does not log /Version, but it still includes the version number in the log file name. + Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "1.0.0.0"), "Base version installed"); + + result = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + result = TestCommon.RunAICLICommand("list", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "List shows the latest available version"); + + result = TestCommon.RunAICLICommand("upgrade", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsFalse(result.StdOut.Contains("2.0.0.0"), "Pin hides latest available version"); + Assert.IsTrue(result.StdOut.Contains("package(s) have pins that prevent upgrade")); + + result = TestCommon.RunAICLICommand("upgrade", "--all"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode, "Upgrade succeeds with nothing to upgrade"); + + Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "1.0.0.0"), "No newer version installed"); + + result = TestCommon.RunAICLICommand("upgrade", "--include-pinned"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "Argument makes available version show up"); + + result = TestCommon.RunAICLICommand("upgrade", "--all --include-pinned"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode, "Upgrade succeeds"); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/Version 2.0.0.0")); + } + + /// <summary> + /// Tests upgrading a package when it has a gating pin that allows updating to another version. + /// </summary> + [Test] + public void UpgradeWithGatingPin() + { + RunCommandResult result; + string installDir = Path.GetTempPath(); + + var pinResult = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller --version 1.0.*"); + Assert.AreEqual(Constants.ErrorCode.S_OK, pinResult.ExitCode); + + result = TestCommon.RunAICLICommand("list", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "List shows the latest available version"); + + result = TestCommon.RunAICLICommand("upgrade", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsFalse(result.StdOut.Contains("2.0.0.0"), "Pin hides latest available version"); + Assert.IsTrue(result.StdOut.Contains("1.0.1.0"), "Version matching pin gated version shows up"); + + result = TestCommon.RunAICLICommand("upgrade", "--all"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode, "Upgrade succeeds"); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/Version 1.0.1.0")); + } + + /// <summary> + /// Tests upgrading a package when it has a gating pin that blocks all other versions. + /// </summary> + [Test] + public void UpgradeWithGatingPinToCurrent() + { + RunCommandResult result; + + result = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller --version 1.0.0.*"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + result = TestCommon.RunAICLICommand("list", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "List shows the latest available version"); + + result = TestCommon.RunAICLICommand("upgrade", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsFalse(result.StdOut.Contains("2.0.0.0"), "Pin hides latest available version"); + Assert.IsTrue(result.StdOut.Contains("package(s) have pins that prevent upgrade")); + + result = TestCommon.RunAICLICommand("upgrade", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_PACKAGE_IS_PINNED, result.ExitCode, "No upgrades available due to pin"); + } + + /// <summary> + /// Tests upgrading a package when it has a blocking pin. + /// </summary> + [Test] + public void UpgradeWithBlockingPin() + { + RunCommandResult result; + + var pinResult = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller --blocking"); + Assert.AreEqual(Constants.ErrorCode.S_OK, pinResult.ExitCode); + + result = TestCommon.RunAICLICommand("list", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "List shows the latest available version"); + + result = TestCommon.RunAICLICommand("upgrade", string.Empty); + Assert.IsFalse(result.StdOut.Contains("2.0.0.0"), "Pin hides latest available version"); + Assert.IsTrue(result.StdOut.Contains("package(s) have pins that prevent upgrade")); + + result = TestCommon.RunAICLICommand("upgrade", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_PACKAGE_IS_PINNED, result.ExitCode, "No upgrades available due to pin"); + } + + /// <summary> + /// Tests upgrading a package when it has a pinning pin and the --force flag is used. + /// </summary> + [Test] + public void ForceUpgradeWithPinningPin() + { + RunCommandResult result; + string installDir = Path.GetTempPath(); + + result = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + result = TestCommon.RunAICLICommand("upgrade", "--force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "--force argument shows latest version"); + + result = TestCommon.RunAICLICommand("upgrade", "--all --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/Version 2.0.0.0"), "--force argument installs last version despite pin"); + } + + /// <summary> + /// Tests upgrading a package when it has a gating pin and the --force flag is used. + /// </summary> + [Test] + public void ForceUpgradeWithGatingPin() + { + RunCommandResult result; + string installDir = Path.GetTempPath(); + + var pinResult = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller --version 1.0.*"); + Assert.AreEqual(Constants.ErrorCode.S_OK, pinResult.ExitCode); + + result = TestCommon.RunAICLICommand("upgrade", "--force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "--force argument shows latest version"); + + result = TestCommon.RunAICLICommand("upgrade", "--all --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode, "Upgrade succeeds"); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/Version 2.0.0.0")); + } + + /// <summary> + /// Tests upgrading a package when it has a blocking pin and the --force flag is used. + /// </summary> + [Test] + public void ForceUpgradeWithBlockingPin() + { + RunCommandResult result; + string installDir = Path.GetTempPath(); + + var pinResult = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller --blocking"); + Assert.AreEqual(Constants.ErrorCode.S_OK, pinResult.ExitCode); + + result = TestCommon.RunAICLICommand("upgrade", "--force"); + Assert.IsTrue(result.StdOut.Contains("2.0.0.0"), "--force argument shows latest version"); + + result = TestCommon.RunAICLICommand("upgrade", "--all --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode, "Upgrade succeeds"); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/Version 2.0.0.0")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -564,7 +564,7 @@ namespace AppInstallerCLIE2ETests /// <param name="installDir">Install directory.</param> /// <param name="expectedContent">Optional expected content.</param> /// <returns>True if success.</returns> - public static bool VerifyTestExeInstalledAndCleanup(string installDir, string expectedContent = null) + public static bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { bool verifyInstallSuccess = true; @@ -581,6 +581,19 @@ namespace AppInstallerCLIE2ETests verifyInstallSuccess = content.Contains(expectedContent); } + return verifyInstallSuccess; + } + + /// <summary> + /// Verify exe installer correctly and then uninstall it. + /// </summary> + /// <param name="installDir">Install directory.</param> + /// <param name="expectedContent">Optional expected content.</param> + /// <returns>True if success.</returns> + public static bool VerifyTestExeInstalledAndCleanup(string installDir, string expectedContent = null) + { + bool verifyInstallSuccess = VerifyTestExeInstalled(installDir, expectedContent); + // Always try clean up and ignore clean up failure var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName); if (File.Exists(uninstallerPath)) diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestExeInstaller +Name: TestExeInstaller +Version: 1.0.1.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /Version 1.0.1.0 + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestExeInstaller +Name: TestExeInstaller +Version: 1.1.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /Version 1.1.0.0 + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml @@ -9,13 +9,13 @@ Installers: - Architecture: x64 InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip InstallerType: zip - ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ProductCode: '{E1880465-8CC2-4033-90AE-DE4E7FDBA26E}' InstallerSha256: <ZIPHASH> NestedInstallerType: exe NestedInstallerFiles: - RelativeFilePath: AppInstallerTestExeInstaller.exe InstallerSwitches: - Custom: /execustom + Custom: /execustom /productID {E1880465-8CC2-4033-90AE-DE4E7FDBA26E} SilentWithProgress: /exeswp Silent: /exesilent Interactive: /exeinteractive diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe_InvalidRelativeFilePath.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe_InvalidRelativeFilePath.yaml @@ -9,10 +9,12 @@ Installers: - Architecture: x64 InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip InstallerType: zip - ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ProductCode: '{E1880465-8CC2-4033-90AE-DE4E7FDBA26E}' InstallerSha256: <ZIPHASH> NestedInstallerType: exe NestedInstallerFiles: - RelativeFilePath: ../../AppInstallerTestExeInstaller.exe + InstallerSwitches: + Custom: /productID {E1880465-8CC2-4033-90AE-DE4E7FDBA26E} ManifestType: singleton ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -81,13 +81,27 @@ namespace AppInstallerCLIE2ETests // Leave the server certificate file if present if (file.Name.ToLower() != Constants.TestSourceServerCertificateFileName) { - file.Delete(); + try + { + file.Delete(); + } + catch + { + // Just ignore errors in this setup step... + } } } foreach (DirectoryInfo dir in directory.GetDirectories()) { - dir.Delete(true); + try + { + dir.Delete(true); + } + catch + { + // Just ignore errors in this setup step... + } } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1331,21 +1331,13 @@ Please specify one of them using the --source option to proceed.</value> <value>This package's version number cannot be determined. To upgrade it anyway, add the argument --include-unknown to your previous command.</value> <comment>{Locked="--include-unknown"}</comment> </data> - <data name="UpgradeUnknownCount" xml:space="preserve"> - <value>packages have version numbers that cannot be determined. Using --include-unknown may show more results.</value> - <comment>{Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. </comment> - </data> - <data name="UpgradeUnknownCountSingle" xml:space="preserve"> - <value>package has a version number that cannot be determined. Using --include-unknown may show more results.</value> - <comment>{Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions.</comment> - </data> <data name="UpgradeUnknownVersionCount" xml:space="preserve"> - <value>package(s) have version numbers that cannot be determined. Use --include-unknown to see all results.</value> - <comment>{Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. </comment> + <value>{0} package(s) have version numbers that cannot be determined. Use --include-unknown to see all results.</value> + <comment>{Locked="{0}","--include-unknown"} {0} is a placeholder that is replaced by an integer number of packages that do not have notated versions. </comment> </data> <data name="UpgradeRequireExplicitCount" xml:space="preserve"> - <value>package(s) is pinned and needs to be explicitly upgraded.</value> - <comment>This string is preceded by a (integer) number of packages that require explicit upgrades.</comment> + <value>{0} package(s) are pinned and need to be explicitly upgraded.</value> + <comment>{Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages that require explicit upgrades.</comment> </data> <data name="InvalidArgumentWithoutQueryError" xml:space="preserve"> <value>The arguments provided can only be used with a query.</value> @@ -1536,14 +1528,14 @@ Please specify one of them using the --source option to proceed.</value> <comment>{Locked="{0}"} The value will be replaced with the feature name</comment> </data> <data name="PinAddCommandLongDescription" xml:space="preserve"> - <value>Add a new pin. A pin can limit the Windows Package Manager from updating a package to specific ranges of versions, or it can prevent it from updating the package altogether. A pinned package may still update on its own and be updated from outside the Windows Package Manager. By default, a pinned package can be updated by mentioning it explicitly in the 'upgrade' command or by adding the '--include-pinned' flag to 'winget upgrade --all'.</value> + <value>Add a new pin. A pin can limit the Windows Package Manager from upgrade a package to specific ranges of versions, or it can prevent it from upgrading the package altogether. A pinned package may still upgrade on its own and be upgraded from outside the Windows Package Manager. By default, a pinned package can be upgraded by mentioning it explicitly in the 'upgrade' command or by adding the '--include-pinned' flag to 'winget upgrade --all'.</value> <comment>{Locked{"'upgrade'"} Locked{"--include-pinned"} Locked{"winget upgrade --all"}</comment> </data> <data name="PinAddCommandShortDescription" xml:space="preserve"> <value>Add a new pin</value> </data> <data name="PinCommandLongDescription" xml:space="preserve"> - <value>Manage package pins with the sub-commands. A pin can limit the Windows Package Manager from updating a package to specific ranges of versions, or it can prevent it from updating the package altogether. A pinned package may still update on its own and be updated from outside the Windows Package Manager.</value> + <value>Manage package pins with the sub-commands. A pin can limit the Windows Package Manager from upgrading a package to specific ranges of versions, or it can prevent it from upgrading the package altogether. A pinned package may still upgrade on its own and be upgraded from outside the Windows Package Manager.</value> </data> <data name="PinCommandShortDescription" xml:space="preserve"> <value>Manage package pins</value> @@ -1570,7 +1562,7 @@ Please specify one of them using the --source option to proceed.</value> <value>Version to which to pin the package. The wildcard '*' can be used as the last version part</value> </data> <data name="PinAddBlockingArgumentDescription" xml:space="preserve"> - <value>Block from updating until the pin is removed, preventing override arguments</value> + <value>Block from upgrading until the pin is removed, preventing override arguments</value> </data> <data name="SettingsExportCommandLongDescription" xml:space="preserve"> <value>Export settings as JSON</value> @@ -1593,15 +1585,16 @@ Please specify one of them using the --source option to proceed.</value> <value>Pin added successfully</value> </data> <data name="PinAlreadyExists" xml:space="preserve"> - <value>The pin already exists</value> + <value>There is already a pin for package {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder that will be replaced by a package name. The message is shown when attempting to add a pin for a package that is already pinned.</comment> </data> <data name="PinExistsOverwriting" xml:space="preserve"> - <value>A pin already exists for this package. Overwriting due to the --force argument.</value> - <comment>{Locked="--force"}</comment> + <value>A pin already exists for package {0}. Overwriting due to the --force argument.</value> + <comment>{Locked="--force"}{Locked="--force","{0}"} {0} is a placeholder that will be replaced by a package name.</comment> </data> <data name="PinExistsUseForceArg" xml:space="preserve"> - <value>A pin already exists for this package. Use the --force argument to overwrite it.</value> - <comment>{Locked="--force"}</comment> + <value>A pin already exists for package {0}. Use the --force argument to overwrite it.</value> + <comment>{Locked="--force","{0}"} {0} is a placeholder that will be replaced by a package name.</comment> </data> <data name="PinResettingAll" xml:space="preserve"> <value>Resetting all current pins.</value> @@ -1663,4 +1656,25 @@ Please specify one of them using the --source option to proceed.</value> <value>Search failed for: {0}</value> <comment>{Locked="{0}"} Error message displayed when the user attempts to search for multiple application packages and one of the searches fails. This message is for generic failures, we have more specific messages for when the search returns no results, or when it returns more than one result. {0} is a placeholder replaced by the package name or query.</comment> </data> + <data name="UpgradeBlockingPinCount" xml:space="preserve"> + <value>{0} package(s) have a blocking pin that needs to be removed before upgrade</value> + <comment>{Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with blocking pins</comment> + </data> + <data name="IncludePinnedArgumentDescription" xml:space="preserve"> + <value>Upgrade packages even if they have a non-blocking pin</value> + </data> + <data name="PinRemovedSuccessfully" xml:space="preserve"> + <value>Pin removed successfully</value> + </data> + <data name="UpgradeIsPinned" xml:space="preserve"> + <value>A newer version was found, but the package has a pin that prevents from upgrading it.</value> + </data> + <data name="PackageIsPinned" xml:space="preserve"> + <value>The package is pinned and cannot be upgraded. Use the 'winget pin' command to view and edit pins. Some pin types can be bypassed with the --include-pinned argument.</value> + <comment>{Locked="winget pin","--include-pinned"} Error shown when we block an upgrade due to the package being pinned</comment> + </data> + <data name="UpgradePinnedByUserCount" xml:space="preserve"> + <value>{0} package(s) have pins that prevent upgrade. Use the 'winget pin' command to view and edit pins. Using the --include-pinned argument may show more results.</value> + <comment>{Locked="winget pin","--include-pinned"} {0} is a placeholder replaced by an integer number of packages</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp @@ -6,12 +6,15 @@ #include "TestHooks.h" #include <CompositeSource.h> #include <Microsoft/SQLiteIndexSource.h> +#include <Microsoft/PinningIndex.h> #include <PackageTrackingCatalogSourceFactory.h> +#include <winget/Pin.h> using namespace std::string_literals; using namespace std::string_view_literals; using namespace TestCommon; using namespace AppInstaller; +using namespace AppInstaller::Pinning; using namespace AppInstaller::Repository; using namespace AppInstaller::Repository::Microsoft; using namespace AppInstaller::Utility; @@ -95,14 +98,14 @@ struct Criteria : public PackageMatchFilter Criteria(PackageMatchField field) : PackageMatchFilter(field, MatchType::Wildcard, ""sv) {} }; -Manifest::Manifest MakeDefaultManifest() +Manifest::Manifest MakeDefaultManifest(std::string_view version = "1.0"sv) { Manifest::Manifest result; result.Id = "Id"; result.DefaultLocalization.Add<Manifest::Localization::PackageName>("Name"); result.DefaultLocalization.Add<Manifest::Localization::Publisher>("Publisher"); - result.Version = "1.0"; + result.Version = version; result.Installers.push_back({}); return result; @@ -110,7 +113,8 @@ Manifest::Manifest MakeDefaultManifest() struct TestPackageHelper { - TestPackageHelper(bool isInstalled) : m_isInstalled(isInstalled), m_manifest(MakeDefaultManifest()) {} + TestPackageHelper(bool isInstalled, std::shared_ptr<ISource> source = {}) : + m_isInstalled(isInstalled), m_manifest(MakeDefaultManifest()), m_source(source) {} TestPackageHelper& WithId(const std::string& id) { @@ -148,11 +152,11 @@ struct TestPackageHelper { if (m_isInstalled) { - m_package = TestPackage::Make(m_manifest, TestPackage::MetadataMap{}); + m_package = TestPackage::Make(m_manifest, TestPackage::MetadataMap{}, std::vector<Manifest::Manifest>(), m_source); } else { - m_package = TestPackage::Make(std::vector<Manifest::Manifest>{ m_manifest }); + m_package = TestPackage::Make(std::vector<Manifest::Manifest>{ m_manifest }, m_source); } } @@ -167,17 +171,18 @@ struct TestPackageHelper private: bool m_isInstalled; Manifest::Manifest m_manifest; + std::shared_ptr<ISource> m_source; std::shared_ptr<TestPackage> m_package; }; TestPackageHelper MakeInstalled() { - return { true }; + return { /* isInstalled */ true}; } -TestPackageHelper MakeAvailable() +TestPackageHelper MakeAvailable(std::shared_ptr<ISource> source) { - return { false }; + return { /* isInstalled */ false, source}; } void RequireIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatchField field, MatchType type, std::optional<std::string> value = {}) @@ -222,7 +227,7 @@ TEST_CASE("CompositeSource_PackageFamilyName_Available", "[CompositeSource]") RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); return result; }; @@ -258,7 +263,7 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") RequireIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, pc); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPC(pc), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC(pc), Criteria()); return result; }; @@ -278,7 +283,7 @@ TEST_CASE("CompositeSource_NameAndPublisher_Match", "[CompositeSource]") RequireIncludes(request.Inclusions, PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact); SearchResult result; - result.Matches.emplace_back(MakeAvailable(), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); return result; }; @@ -298,8 +303,8 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable().WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); return result; }; @@ -308,8 +313,8 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") REQUIRE(result.Matches.size() == 1); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); - REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Name).get() == name); - REQUIRE(!Version(result.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Version)).IsUnknown()); + REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Name).get() == name); + REQUIRE(!Version(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Version)).IsUnknown()); } TEST_CASE("CompositeSource_MultiMatch_DoesNotFindStrongMatch", "[CompositeSource]") @@ -319,8 +324,8 @@ TEST_CASE("CompositeSource_MultiMatch_DoesNotFindStrongMatch", "[CompositeSource setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable().WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); return result; }; @@ -335,10 +340,10 @@ TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") { std::string pfn = "sortof_apfn"; + CompositeTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); - CompositeTestSetup setup; setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -380,13 +385,13 @@ TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); return result; }; @@ -402,13 +407,13 @@ TEST_CASE("CompositeSource_FoundByAvailableRootSearch_NotInstalled", "[Composite std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Available->Everything.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); return result; }; @@ -423,10 +428,10 @@ TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") MatchType originalType = MatchType::Wildcard; MatchType type = MatchType::Exact; + CompositeTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); - CompositeTestSetup setup; setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) @@ -488,7 +493,7 @@ TEST_CASE("CompositePackage_PropertyFromAvailable", "[CompositeSource]") setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithId(id), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(id), Criteria()); return result; }; @@ -515,7 +520,7 @@ TEST_CASE("CompositePackage_AvailableVersions_ChannelFilteredOut", "[CompositeSo hasChannel.Version = "2.0"; SearchResult result; - result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }), Criteria()); + result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }, setup.Available), Criteria()); REQUIRE(result.Matches.back().Package->GetAvailableVersionKeys().size() == 2); return result; }; @@ -527,11 +532,11 @@ TEST_CASE("CompositePackage_AvailableVersions_ChannelFilteredOut", "[CompositeSo REQUIRE(versionKeys.size() == 1); REQUIRE(versionKeys[0].Channel.empty()); - auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(); + auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); REQUIRE(latestVersion); REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get().empty()); - REQUIRE(!result.Matches[0].Package->IsUpdateAvailable()); + REQUIRE(!result.Matches[0].Package->IsUpdateAvailable(PinBehavior::IgnorePins)); } TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[CompositeSource]") @@ -551,7 +556,7 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite hasChannel.Version = "2.0"; SearchResult result; - result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }), Criteria()); + result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }, setup.Available), Criteria()); REQUIRE(result.Matches.back().Package->GetAvailableVersionKeys().size() == 2); return result; }; @@ -563,15 +568,18 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite REQUIRE(versionKeys.size() == 1); REQUIRE(versionKeys[0].Channel == channel); - auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(); + auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); REQUIRE(latestVersion); REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get() == channel); - REQUIRE(result.Matches[0].Package->IsUpdateAvailable()); + REQUIRE(result.Matches[0].Package->IsUpdateAvailable(PinBehavior::IgnorePins)); } -TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSource]") +TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource]") { + TestCommon::TestUserSettings testSettings; + testSettings.Set<Settings::Setting::EFPinning>(true); + std::string pfn = "sortof_apfn"; std::string firstName = "Name1"; std::string secondName = "Name2"; @@ -587,7 +595,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithDefaultName(firstName), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(firstName), Criteria()); return result; }; @@ -596,7 +604,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); return result; }; @@ -604,8 +612,8 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour REQUIRE(result.Matches.size() == 1); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); - REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); - REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Name).get() == firstName); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 2); + REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Name).get() == firstName); } TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSource]") @@ -625,7 +633,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(secondName), Criteria()); return result; }; @@ -634,7 +642,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou REQUIRE(result.Matches.size() == 1); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); - REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Name).get() == secondName); + REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Name).get() == secondName); } TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[CompositeSource]") @@ -656,8 +664,8 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); - secondAvailable->Everything.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + secondAvailable->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); SearchResult result = setup.Search(); @@ -670,7 +678,7 @@ TEST_CASE("CompositeSource_IsSame", "[CompositeSource]") { CompositeTestSetup setup; setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); - setup.Available->Everything.Matches.emplace_back(MakeAvailable().WithPFN("sortof_apfn"), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN("sortof_apfn"), Criteria()); SearchResult result1 = setup.Search(); REQUIRE(result1.Matches.size() == 1); @@ -690,7 +698,7 @@ TEST_CASE("CompositeSource_AvailableSearchFailure", "[CompositeSource]") AvailableSucceeds->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(MakeAvailable({}).WithPFN(pfn), Criteria()); return result; }; @@ -706,7 +714,7 @@ TEST_CASE("CompositeSource_AvailableSearchFailure", "[CompositeSource]") REQUIRE(result.Matches.size() == 1); - auto pfns = result.Matches[0].Package->GetLatestAvailableVersion()->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); + auto pfns = result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); REQUIRE(pfns.size() == 1); REQUIRE(pfns[0] == pfn); @@ -734,7 +742,7 @@ TEST_CASE("CompositeSource_InstalledToAvailableCorrelationSearchFailure", "[Comp CompositeTestSetup setup; setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); - setup.Available->Everything.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); std::shared_ptr<ComponentTestSource> AvailableFails = std::make_shared<ComponentTestSource>(); AvailableFails->SearchFunction = [&](const SearchRequest&) -> SearchResult { THROW_HR(expectedHR); }; @@ -772,7 +780,7 @@ TEST_CASE("CompositeSource_InstalledAvailableSearchFailure", "[CompositeSource]" setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); return result; }; @@ -812,10 +820,10 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; + CompositeWithTrackingTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); - CompositeWithTrackingTestSetup setup; setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -852,7 +860,7 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") REQUIRE(result.Matches[0].Package); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetInstalledVersion()->GetSource().GetIdentifier() == setup.Available->Details.Identifier); - REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion()); + REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)); } TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", "[CompositeSource]") @@ -860,10 +868,10 @@ TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; + CompositeWithTrackingTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); - CompositeWithTrackingTestSetup setup; setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -921,10 +929,10 @@ TEST_CASE("CompositeSource_TrackingFound_AvailableNot", "[CompositeSource]") std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; + CompositeWithTrackingTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); - CompositeWithTrackingTestSetup setup; setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -943,18 +951,19 @@ TEST_CASE("CompositeSource_TrackingFound_AvailableNot", "[CompositeSource]") REQUIRE(result.Matches[0].Package); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetInstalledVersion()->GetSource().GetIdentifier() == setup.Available->Details.Identifier); - REQUIRE(!result.Matches[0].Package->GetLatestAvailableVersion()); + REQUIRE(!result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)); } TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") { + CompositeWithTrackingTestSetup setup; + std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); - CompositeWithTrackingTestSetup setup; setup.Installed->SearchFunction = [&](const SearchRequest& request) { RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); @@ -983,7 +992,7 @@ TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") REQUIRE(result.Matches[0].Package); REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetInstalledVersion()->GetSource().GetIdentifier() == setup.Available->Details.Identifier); - REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion()); + REQUIRE(result.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)); } TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") @@ -991,10 +1000,10 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; + CompositeWithTrackingTestSetup setup; auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); - CompositeWithTrackingTestSetup setup; setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); setup.Tracking->GetIndex().AddManifest(availablePackage); @@ -1007,7 +1016,7 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") TEST_CASE("CompositeSource_NullInstalledVersion", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeAvailable(), Criteria()); + setup.Installed->Everything.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); // We are mostly testing to see if a null installed version causes an AV or not SearchResult result = setup.Search(); @@ -1023,3 +1032,288 @@ TEST_CASE("CompositeSource_NullAvailableVersion", "[CompositeSource]") SearchResult result = setup.Search(); REQUIRE(result.Matches.size() == 1); } + +struct ExpectedResultForPinBehavior +{ + ExpectedResultForPinBehavior(bool isUpdateAvailable, std::optional<std::string> latestAvailableVersion) + : IsUpdateAvailable(isUpdateAvailable), LatestAvailableVersion(latestAvailableVersion) {} + ExpectedResultForPinBehavior() {} + + bool IsUpdateAvailable = false; + std::optional<std::string> LatestAvailableVersion; +}; + +struct ExpectedResultsForPinning +{ + std::map<PinBehavior, ExpectedResultForPinBehavior> ResultsForPinBehavior; + std::vector<PackageVersionKey> AvailableVersions; +}; + +void RequireExpectedResultsWithPin(std::shared_ptr<IPackage> package, const ExpectedResultsForPinning& expectedResult) +{ + for (const auto& entry : expectedResult.ResultsForPinBehavior) + { + auto pinBehavior = entry.first; + const auto& result = entry.second; + + REQUIRE(package->IsUpdateAvailable(pinBehavior) == result.IsUpdateAvailable); + + auto latestAvailable = package->GetLatestAvailableVersion(pinBehavior); + if (result.LatestAvailableVersion.has_value()) + { + REQUIRE(latestAvailable); + REQUIRE(latestAvailable->GetManifest().Version == result.LatestAvailableVersion.value()); + } + else + { + REQUIRE(!latestAvailable); + } + } + + auto availableVersionKeys = package->GetAvailableVersionKeys(); + REQUIRE(availableVersionKeys.size() == expectedResult.AvailableVersions.size()); + for (size_t i = 0; i < availableVersionKeys.size(); ++i) + { + REQUIRE(availableVersionKeys[i].SourceId == expectedResult.AvailableVersions[i].SourceId); + REQUIRE(availableVersionKeys[i].Version == expectedResult.AvailableVersions[i].Version); + REQUIRE(availableVersionKeys[i].PinnedState == expectedResult.AvailableVersions[i].PinnedState); + REQUIRE(package->GetAvailableVersion(expectedResult.AvailableVersions[i])); + } +} + +TEST_CASE("CompositeSource_PinnedAvailable", "[CompositeSource][PinFlow]") +{ + // We use an installed package that has 3 available versions: v1.0.0, v1.0.1 and v1.1.0. + // Installed is v1.0.1 + // We then test the 4 possible pin states (unpinned, Pinned, Blocked, Gated) + // with the 3 possible pin search behaviors (ignore, consider, include pinned) + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + TestUserSettings userSettings; + userSettings.Set<Settings::Setting::EFPinning>(true); + + CompositeTestSetup setup; + + auto installedPackage = TestPackage::Make(MakeDefaultManifest("1.0.1"sv), TestPackage::MetadataMap{}); + setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + auto manifest1 = MakeDefaultManifest("1.0.0"sv); + auto manifest2 = MakeDefaultManifest("1.0.1"sv); + auto manifest3 = MakeDefaultManifest("1.1.0"sv); + auto package = TestPackage::Make( + std::vector<Manifest::Manifest>{ manifest1, manifest2, manifest3 }, + setup.Available); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + ExpectedResultsForPinning expectedResult; + // The result when ignoring pins is always the same + expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "1.1.0" }; + + PinKey pinKey("Id", setup.Available->Details.Identifier); + auto pinningIndex = PinningIndex::OpenOrCreateDefault(); + REQUIRE(pinningIndex); + + SECTION("Unpinned") + { + // If there are no pins, the result should not change if we consider them + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins]; + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins]; + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "1.1.0", "", Pinning::PinType::Unknown }, + { "AvailableTestSource1", "1.0.1", "", Pinning::PinType::Unknown }, + { "AvailableTestSource1", "1.0.0", "", Pinning::PinType::Unknown }, + }; + } + SECTION("Pinned") + { + pinningIndex->AddPin(Pin::CreatePinningPin(PinKey{ pinKey })); + + // Pinning pins are ignored with --include-pinned + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins]; + + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ false, /* LatestAvailableVersion */ {} }; + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "1.1.0", "", Pinning::PinType::Pinning }, + { "AvailableTestSource1", "1.0.1", "", Pinning::PinType::Pinning }, + { "AvailableTestSource1", "1.0.0", "", Pinning::PinType::Pinning }, + }; + } + SECTION("Blocked") + { + pinningIndex->AddPin(Pin::CreateBlockingPin(PinKey{ pinKey })); + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ false, /* LatestAvailableVersion */ {} }; + + // Blocking pins are not affected by --include-pinned + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins]; + + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "1.1.0", "", Pinning::PinType::Blocking }, + { "AvailableTestSource1", "1.0.1", "", Pinning::PinType::Blocking }, + { "AvailableTestSource1", "1.0.0", "", Pinning::PinType::Blocking }, + }; + } + SECTION("Gated to 1.*") + { + pinningIndex->AddPin(Pin::CreateGatingPin(PinKey{ pinKey }, GatedVersion{ "1.*"sv })); + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "1.1.0" }; + + // Gating pins are not affected by --include-pinned + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins]; + + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "1.1.0", "", Pinning::PinType::Unknown }, + { "AvailableTestSource1", "1.0.1", "", Pinning::PinType::Unknown }, + { "AvailableTestSource1", "1.0.0", "", Pinning::PinType::Unknown }, + }; + } + SECTION("Gated to 1.0.*") + { + pinningIndex->AddPin(Pin::CreateGatingPin(PinKey{ pinKey }, GatedVersion{ "1.0.*"sv })); + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ false, /* LatestAvailableVersion */ "1.0.1"}; + + // Gating pins are not affected by --include-pinned + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins]; + + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "1.1.0", "", Pinning::PinType::Gating }, + { "AvailableTestSource1", "1.0.1", "", Pinning::PinType::Unknown }, + { "AvailableTestSource1", "1.0.0", "", Pinning::PinType::Unknown }, + }; + } + + SearchResult result = setup.Search(); + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + + RequireExpectedResultsWithPin(package, expectedResult); +} + +TEST_CASE("CompositeSource_OneSourcePinned", "[CompositeSource][PinFlow]") +{ + // We use an installed package that has 2 available sources. + // If one of them is pinned, we should still get the updates from the other one. + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + TestUserSettings userSettings; + userSettings.Set<Settings::Setting::EFPinning>(true); + + CompositeTestSetup setup; + + auto installedPackage = TestPackage::Make(MakeDefaultManifest("1.0"sv), TestPackage::MetadataMap{}); + setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make(std::vector<Manifest::Manifest>{ MakeDefaultManifest("2.0"sv) }, setup.Available); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>("SecondTestSource"); + setup.Composite.AddAvailableSource(Source{ secondAvailable }); + secondAvailable->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make(std::vector<Manifest::Manifest>{ MakeDefaultManifest("1.1"sv) }, secondAvailable); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + { + PinKey pinKey("Id", setup.Available->Details.Identifier); + auto pinningIndex = PinningIndex::OpenOrCreateDefault(); + REQUIRE(pinningIndex); + pinningIndex->AddPin(Pin::CreatePinningPin(PinKey{ pinKey })); + } + + ExpectedResultsForPinning expectedResult; + expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "2.0" }; + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "1.1" }; + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "2.0" }; + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Pinning }, + { "SecondTestSource", "1.1", "", Pinning::PinType::Unknown }, + }; + + SearchResult result = setup.Search(); + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + RequireExpectedResultsWithPin(package, expectedResult); +} + +TEST_CASE("CompositeSource_OneSourceGated", "[CompositeSource][PinFlow]") +{ + // We use an installed package that has 2 available sources. + // If one of them has a gating pin, we should still get the updates from it + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + TestUserSettings userSettings; + userSettings.Set<Settings::Setting::EFPinning>(true); + + CompositeTestSetup setup; + + auto installedPackage = TestPackage::Make(MakeDefaultManifest("1.0"sv), TestPackage::MetadataMap{}); + setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make( + std::vector<Manifest::Manifest>{ + MakeDefaultManifest("2.0"sv), + MakeDefaultManifest("1.2"sv), + }, + setup.Available); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>("SecondTestSource"); + setup.Composite.AddAvailableSource(Source{ secondAvailable }); + secondAvailable->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make(std::vector<Manifest::Manifest>{ MakeDefaultManifest("1.1"sv) }, secondAvailable); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + { + PinKey pinKey("Id", setup.Available->Details.Identifier); + auto pinningIndex = PinningIndex::OpenOrCreateDefault(); + REQUIRE(pinningIndex); + pinningIndex->AddPin(Pin::CreateGatingPin(PinKey{ pinKey }, GatedVersion{ "1.*"sv })); + } + + ExpectedResultsForPinning expectedResult; + expectedResult.ResultsForPinBehavior[PinBehavior::IgnorePins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "2.0" }; + expectedResult.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "1.2" }; + expectedResult.ResultsForPinBehavior[PinBehavior::IncludePinned] = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "1.2" }; + expectedResult.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Gating }, + { "AvailableTestSource1", "1.2", "", Pinning::PinType::Unknown }, + { "SecondTestSource", "1.1", "", Pinning::PinType::Unknown }, + }; + + SearchResult result = setup.Search(); + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + RequireExpectedResultsWithPin(package, expectedResult); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/PackageTrackingCatalog.cpp b/src/AppInstallerCLITests/PackageTrackingCatalog.cpp @@ -87,7 +87,7 @@ TEST_CASE("TrackingCatalog_Install", "[tracking_catalog]") SearchResult resultAfter = catalog.Search(request); REQUIRE(resultAfter.Matches.size() == 1); - auto trackingVersion = resultAfter.Matches[0].Package->GetLatestAvailableVersion(); + auto trackingVersion = resultAfter.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); REQUIRE(trackingVersion); auto metadata = trackingVersion->GetMetadata(); @@ -113,7 +113,7 @@ TEST_CASE("TrackingCatalog_Reinstall", "[tracking_catalog]") SearchResult resultBefore = catalog.Search(request); REQUIRE(resultBefore.Matches.size() == 1); - REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Name) == + REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Name) == manifest.DefaultLocalization.Get<Localization::PackageName>()); // Change name @@ -124,7 +124,7 @@ TEST_CASE("TrackingCatalog_Reinstall", "[tracking_catalog]") SearchResult resultAfter = catalog.Search(request); REQUIRE(resultAfter.Matches.size() == 1); - REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Name) == + REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Name) == newName); } @@ -147,7 +147,7 @@ TEST_CASE("TrackingCatalog_Upgrade", "[tracking_catalog]") SearchResult resultBefore = catalog.Search(request); REQUIRE(resultBefore.Matches.size() == 1); - REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Version) == + REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Version) == manifest.Version); // Change name @@ -157,7 +157,7 @@ TEST_CASE("TrackingCatalog_Upgrade", "[tracking_catalog]") SearchResult resultAfter = catalog.Search(request); REQUIRE(resultAfter.Matches.size() == 1); - REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Version) == + REQUIRE(resultBefore.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetProperty(PackageVersionProperty::Version) == manifest.Version); } diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp @@ -2,15 +2,18 @@ // Licensed under the MIT License. #include "pch.h" #include "WorkflowCommon.h" +#include "TestHooks.h" #include <Commands/PinCommand.h> #include <Workflows/PinFlow.h> #include <Microsoft/PinningIndex.h> #include <AppInstallerRuntime.h> +#include <AppInstallerVersions.h> using namespace TestCommon; using namespace AppInstaller::CLI; using namespace AppInstaller::CLI::Workflow; using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Utility; using namespace AppInstaller::Pinning; void OverrideForOpenPinningIndex(TestContext& context, const std::filesystem::path& indexPath) @@ -27,6 +30,7 @@ void OverrideForOpenPinningIndex(TestContext& context, const std::filesystem::pa TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") { TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); std::ostringstream pinAddOutput; TestContext addContext{ pinAddOutput, std::cin }; @@ -146,6 +150,9 @@ TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") TEST_CASE("PinFlow_Add_NotFound", "[PinFlow][workflow]") { + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + std::ostringstream pinAddOutput; TestContext addContext{ pinAddOutput, std::cin }; OverrideForCompositeInstalledSource(addContext, CreateTestSource({})); @@ -161,6 +168,7 @@ TEST_CASE("PinFlow_Add_NotFound", "[PinFlow][workflow]") TEST_CASE("PinFlow_ListEmpty", "[PinFlow][workflow]") { TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); std::ostringstream pinListOutput; TestContext listContext{ pinListOutput, std::cin }; @@ -177,6 +185,7 @@ TEST_CASE("PinFlow_ListEmpty", "[PinFlow][workflow]") TEST_CASE("PinFlow_RemoveNonExisting", "[PinFlow][workflow]") { TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); std::ostringstream pinRemoveOutput; TestContext removeContext{ pinRemoveOutput, std::cin }; @@ -194,6 +203,7 @@ TEST_CASE("PinFlow_RemoveNonExisting", "[PinFlow][workflow]") TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]") { TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); std::ostringstream pinResetOutput; TestContext resetContext{ pinResetOutput, std::cin }; diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp @@ -99,7 +99,7 @@ TEST_CASE("PinningIndex_AddUpdateRemove", "[pinningIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - Pin pin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*" }); + Pin pin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*"sv }); Pin updatedPin = Pin::CreatePinningPin({ "pkgId", "srcId" }); { @@ -158,7 +158,7 @@ TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - Pin pin = Pin::CreateGatingPin({ "pkg", "src" }, { "1.*" }); + Pin pin = Pin::CreateGatingPin({ "pkg", "src" }, { "1.*"sv }); PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); index.AddPin(pin); diff --git a/src/AppInstallerCLITests/SQLiteIndexSource.cpp b/src/AppInstallerCLITests/SQLiteIndexSource.cpp @@ -86,7 +86,7 @@ TEST_CASE("SQLiteIndexSource_Id", "[sqliteindexsource]") auto results = source->Search(request); REQUIRE(results.Matches.size() == 1); REQUIRE(results.Matches[0].Package); - auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(); + auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Id).get() == manifest.Id); } @@ -107,7 +107,7 @@ TEST_CASE("SQLiteIndexSource_Name", "[sqliteindexsource]") auto results = source->Search(request); REQUIRE(results.Matches.size() == 1); REQUIRE(results.Matches[0].Package); - auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(); + auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(PinBehavior::IgnorePins); REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Name).get() == manifest.DefaultLocalization.Get<Localization::PackageName>()); } diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -34,6 +34,11 @@ namespace AppInstaller void TestHook_ClearSourceFactoryOverrides(); } + namespace Repository::Microsoft + { + void TestHook_SetPinningIndex_Override(std::optional<std::filesystem::path>&& indexPath); + } + namespace Logging { void TestHook_SetTelemetryOverride(std::shared_ptr<TelemetryTraceLogger> ttl); @@ -88,4 +93,17 @@ namespace TestHook private: bool m_status; }; + + struct SetPinningIndex_Override + { + SetPinningIndex_Override(const std::filesystem::path& indexPath) + { + AppInstaller::Repository::Microsoft::TestHook_SetPinningIndex_Override(indexPath); + } + + ~SetPinningIndex_Override() + { + AppInstaller::Repository::Microsoft::TestHook_SetPinningIndex_Override({}); + } + }; } \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -182,12 +182,12 @@ namespace TestCommon std::vector<PackageVersionKey> result; for (const auto& version : AvailableVersions) { - result.emplace_back(PackageVersionKey("", version->GetProperty(PackageVersionProperty::Version).get(), version->GetProperty(PackageVersionProperty::Channel).get())); + result.emplace_back(PackageVersionKey(version->GetSource().GetIdentifier(), version->GetProperty(PackageVersionProperty::Version).get(), version->GetProperty(PackageVersionProperty::Channel).get())); } return result; } - std::shared_ptr<IPackageVersion> TestPackage::GetLatestAvailableVersion() const + std::shared_ptr<IPackageVersion> TestPackage::GetLatestAvailableVersion(PinBehavior) const { if (AvailableVersions.empty()) { @@ -211,7 +211,7 @@ namespace TestCommon return {}; } - bool TestPackage::IsUpdateAvailable() const + bool TestPackage::IsUpdateAvailable(PinBehavior) const { if (InstalledVersion && !AvailableVersions.empty()) { diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -64,9 +64,9 @@ namespace TestCommon AppInstaller::Utility::LocIndString GetProperty(AppInstaller::Repository::PackageProperty property) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetInstalledVersion() const override; std::vector<AppInstaller::Repository::PackageVersionKey> GetAvailableVersionKeys() const override; - std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetLatestAvailableVersion() const override; + std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetLatestAvailableVersion(AppInstaller::Repository::PinBehavior) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetAvailableVersion(const AppInstaller::Repository::PackageVersionKey& versionKey) const override; - bool IsUpdateAvailable() const override; + bool IsUpdateAvailable(AppInstaller::Repository::PinBehavior) const override; bool IsSame(const IPackage* other) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> InstalledVersion; diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -299,7 +299,7 @@ TEST_CASE("UpdateFlow_NoArgs_UnknownVersion", "[UpdateFlow][workflow]") INFO(updateOutput.str()); // Verify --include-unknown help text is displayed if update is executed with no args and an unknown version package is available for upgrade. - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeUnknownVersionCount).get()) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::String::UpgradeUnknownVersionCount(1)) != std::string::npos); } TEST_CASE("UpdateFlow_IncludeUnknown", "[UpdateFlow][workflow]") @@ -323,7 +323,7 @@ TEST_CASE("UpdateFlow_IncludeUnknown", "[UpdateFlow][workflow]") INFO(updateOutput.str()); // Verify unknown version package is displayed available for upgrade. - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeUnknownVersionCount).get()) == std::string::npos); + REQUIRE(updateOutput.str().find(Resource::String::UpgradeUnknownVersionCount(1)) == std::string::npos); REQUIRE(updateOutput.str().find("unknown") != std::string::npos); } @@ -540,7 +540,7 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") INFO(updateOutput.str()); // Verify that --include-unknown help message is displayed. - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeUnknownVersionCount).get()) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::String::UpgradeUnknownVersionCount(1)) != std::string::npos); REQUIRE(updateOutput.str().find("AppInstallerCliTest.TestExeUnknownVersion") == std::string::npos); // Verify installers are called. @@ -801,7 +801,7 @@ TEST_CASE("UpdateFlow_RequireExplicit", "[UpdateFlow][workflow]") REQUIRE(pinnedPackagesHeaderPosition != std::string::npos); REQUIRE(pinnedPackageLinePosition != std::string::npos); REQUIRE(pinnedPackagesHeaderPosition < pinnedPackageLinePosition); - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeRequireExplicitCount)) == std::string::npos); + REQUIRE(updateOutput.str().find(Resource::String::UpgradeRequireExplicitCount(1)) == std::string::npos); } SECTION("Upgrade all except pinned") @@ -820,7 +820,7 @@ TEST_CASE("UpdateFlow_RequireExplicit", "[UpdateFlow][workflow]") auto s = updateOutput.str(); // Verify message is printed for skipped package - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeRequireExplicitCount)) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::String::UpgradeRequireExplicitCount(1)) != std::string::npos); // Verify package is not installed, but all others are REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp @@ -317,3 +317,21 @@ TEST_CASE("VersionRange", "[versions]") REQUIRE(VersionRange{ Version{ "0.5" }, Version{ "1.0" } } < VersionRange{ Version{ "1.5" }, Version{ "2.0" } }); REQUIRE_FALSE(VersionRange{ Version{ "1.5" }, Version{ "2.0" } } < VersionRange{ Version{ "0.5" }, Version{ "1.0" } }); } + +TEST_CASE("GatedVersion", "[versions]") +{ + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.0.1" })); + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.0" })); + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1" })); + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.0.alpha" })); + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.0.1.2.3" })); + REQUIRE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.0.*" })); + REQUIRE_FALSE(GatedVersion("1.0.*"sv).IsValidVersion({ "1.1.1" })); + + REQUIRE(GatedVersion("1.*.*"sv).IsValidVersion({ "1.*.1" })); + REQUIRE(GatedVersion("1.*.*"sv).IsValidVersion({ "1.*.*" })); + REQUIRE_FALSE(GatedVersion("1.*.*"sv).IsValidVersion({ "1.1.1" })); + + REQUIRE(GatedVersion("1.0.1"sv).IsValidVersion({ "1.0.1" })); + REQUIRE_FALSE(GatedVersion("1.0.1"sv).IsValidVersion({ "1.1.1" })); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Pin.cpp b/src/AppInstallerCommonCore/Pin.cpp @@ -19,50 +19,51 @@ namespace AppInstaller::Pinning return "Pinning"sv; case PinType::Gating: return "Gating"sv; + case PinType::PinnedByManifest: + return "PinnedByManifest"sv; case PinType::Unknown: default: return "Unknown"; } } - Pin Pin::CreateBlockingPin(PinKey&& pinKey) + PinType ConvertToPinTypeEnum(std::string_view in) { - return { PinType::Blocking, std::move(pinKey) }; + if (Utility::CaseInsensitiveEquals(in, "Blocking"sv)) + { + return PinType::Blocking; } - - Pin Pin::CreatePinningPin(PinKey&& pinKey) + else if (Utility::CaseInsensitiveEquals(in, "Pinning"sv)) { - return { PinType::Pinning, std::move(pinKey) }; + return PinType::Pinning; } - - Pin Pin::CreateGatingPin(PinKey&& pinKey, GatedVersion&& gatedVersion) + else if (Utility::CaseInsensitiveEquals(in, "Gating"sv)) { - return { PinType::Gating, std::move(pinKey), std::move(gatedVersion) }; + return PinType::Gating; } - - PinType Pin::GetType() const + else if (Utility::CaseInsensitiveEquals(in, "PinnedByManifest"sv)) { - return m_type; + return PinType::PinnedByManifest; } - - const PinKey& Pin::GetKey() const + else { - return m_key; + return PinType::Unknown; + } } - const AppInstaller::Manifest::Manifest::string_t& Pin::GetPackageId() const + Pin Pin::CreateBlockingPin(PinKey&& pinKey) { - return m_key.PackageId; + return { PinType::Blocking, std::move(pinKey) }; } - std::string_view Pin::GetSourceId() const + Pin Pin::CreatePinningPin(PinKey&& pinKey) { - return m_key.SourceId; + return { PinType::Pinning, std::move(pinKey) }; } - Utility::GatedVersion Pin::GetGatedVersion() const + Pin Pin::CreateGatingPin(PinKey&& pinKey, GatedVersion&& gatedVersion) { - return m_gatedVersion; + return { PinType::Gating, std::move(pinKey), std::move(gatedVersion) }; } bool Pin::operator==(const Pin& other) const diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -95,7 +95,7 @@ namespace AppInstaller::Utility std::string Other; }; - // Gets the part breakdown for a given version; used for tests. + // Gets the part breakdown for a given version. const std::vector<Part>& GetParts() const { return m_parts; } // Gets the part at the given index; or the implied zero part if past the end. @@ -172,16 +172,23 @@ namespace AppInstaller::Utility // A range of versions indicated by a version and optionally a wildcard at the end. struct GatedVersion { - // TODO - // For now, using dummy implementation that just holds a string GatedVersion() {} - GatedVersion(std::string_view s) : m_tmp(s) {} - std::string ToString() const { return m_tmp; } + GatedVersion(Version&& version) : m_version(std::move(version)) {} + GatedVersion(const Version& version) : m_version(version) {} + GatedVersion(std::string_view versionString) : m_version(std::string{ versionString }) {} + GatedVersion(const std::string& versionString) : m_version(versionString) {} - bool operator==(const GatedVersion& other) const { return m_tmp == other.m_tmp; } + // Determines whether a given version falls within this Gated version. + // I.e., whether it matches up to the wildcard + bool IsValidVersion(Version version) const; + + bool operator==(const GatedVersion& other) const { return m_version == other.m_version; } + const std::string& ToString() const { return m_version.ToString(); } private: - std::string m_tmp; + // Hold the version string as a Version object that makes it easy to access each of + // the version's parts. The real magic is in IsValidVersion() + Version m_version; }; // A channel string; existing solely to give a type. diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -8,18 +8,23 @@ namespace AppInstaller::Pinning { enum class PinType { + // Unknown pin type or not pinned Unknown, + // Pinned by the manifest using the RequiresExplicitUpgrade field. + // Behaves the same as Pinning pins + PinnedByManifest, // The package is blocked from 'upgrade --all' and 'upgrade <package>'. // User has to unblock to allow update. Blocking, // The package is excluded from 'upgrade --all', unless '--include-pinned' is added. // 'upgrade <package>' is not blocked. Pinning, - // The package is pinned to a specific version. + // The package is pinned to a specific version range. Gating, }; std::string_view ToString(PinType type); + PinType ConvertToPinTypeEnum(std::string_view in); // The set of values needed to uniquely identify a Pin struct PinKey @@ -36,6 +41,10 @@ namespace AppInstaller::Pinning { return !(*this == other); } + bool operator<(const PinKey& other) const + { + return PackageId < other.PackageId || (PackageId == other.PackageId && SourceId < other.SourceId); + } Manifest::Manifest::string_t PackageId; std::string SourceId; @@ -47,11 +56,11 @@ namespace AppInstaller::Pinning static Pin CreatePinningPin(PinKey&& pinKey); static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion); - PinType GetType() const; - const PinKey& GetKey() const; - const Manifest::Manifest::string_t& GetPackageId() const; - std::string_view GetSourceId() const; - Utility::GatedVersion GetGatedVersion() const; + PinType GetType() const { return m_type; } + const PinKey& GetKey() const { return m_key; } + const Manifest::Manifest::string_t& GetPackageId() const { return m_key.PackageId; } + const std::string& GetSourceId() const { return m_key.SourceId; } + const Utility::GatedVersion& GetGatedVersion() const { return m_gatedVersion; } bool operator==(const Pin& other) const; diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -498,6 +498,49 @@ namespace AppInstaller::Utility return m_maxVersion; } + bool GatedVersion::IsValidVersion(Version version) const + { + auto gateParts = m_version.GetParts(); + if (gateParts.empty()) + { + return false; + } + + if (gateParts.back() != Version::Part("*")) + { + // Without wildcards, revert to direct comparison + return m_version == version; + } + + auto versionParts = version.GetParts(); + for (size_t i = 0; i < gateParts.size() - 1; ++i) + { + if (versionParts.size() > i) + { + if (gateParts[i] == versionParts[i]) + { + continue; + } + else + { + // Mismatch with the gated version + return false; + } + } + else + { + // Assume trailing 0s on the version + if (gateParts[i] != Version::Part(0)) + { + return false; + } + } + } + + // All version parts matched + return true; + } + bool HasOverlapInVersionRanges(const std::vector<VersionRange>& ranges) { for (size_t i = 0; i < ranges.size(); i++) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -2,6 +2,11 @@ // Licensed under the MIT License. #include "pch.h" #include "CompositeSource.h" +#include "Microsoft/PinningIndex.h" +#include <winget/ExperimentalFeature.h> + +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Settings; namespace AppInstaller::Repository { @@ -17,6 +22,46 @@ namespace AppInstaller::Repository }; } + Pinning::PinKey GetPinKey(IPackage* availablePackage) + { + return { + availablePackage->GetProperty(PackageProperty::Id).get(), + availablePackage->GetLatestAvailableVersion(PinBehavior::IgnorePins)->GetSource().GetIdentifier() + }; + } + + std::optional<PackageVersionKey> GetLatestAvailableVersionKeySatisfyingPin(const std::vector<PackageVersionKey>& availableVersionKeys, PinBehavior pinBehavior) + { + if (availableVersionKeys.empty()) + { + return {}; + } + + std::optional<PackageVersionKey> pvk; + if (pinBehavior == PinBehavior::IgnorePins) + { + pvk = availableVersionKeys.front(); + } + else + { + // Skip until we find a version that isn't pinned + for (const auto& availableVersion : availableVersionKeys) + { + if (availableVersion.PinnedState == Pinning::PinType::Blocking || + availableVersion.PinnedState == Pinning::PinType::Gating || + (availableVersion.PinnedState == Pinning::PinType::Pinning && pinBehavior != PinBehavior::IncludePinned)) + { + continue; + } + + pvk = availableVersion; + break; + } + } + + return pvk; + } + // Returns true for fields that provide a strong match; one that is not based on a heuristic. bool IsStrongMatchField(PackageMatchField field) { @@ -123,8 +168,12 @@ namespace AppInstaller::Repository return { resultTime, std::move(resultVersion) }; } + // An installed package's version reported in ARP does not necessarily match the versions used for the manifest. + // This function uses the data in the manifest to map the installed version string to the version used by the manifest. + // // TODO: Note: Currently this function assumes the all versions in the available package is from one source. - // If one day we start adding support for available package from multiple sources, this function needs to be revisited. + // Even though a composite package can have available packages from multiple sources, we only call this function + // for the default (first) available package. If we ever need to consider other sources, this function needs to be revisited. std::string GetMappedInstalledVersion(const std::string& installedVersion, const std::shared_ptr<IPackage>& availablePackage) { // Stores raw versions value strings to run a preliminary check whether version mapping is needed. @@ -320,11 +369,134 @@ namespace AppInstaller::Repository std::shared_ptr<IPackageVersion> m_trackingPackageVersion; }; + // Wrapper around an available package to add pinning functionality for composite packages. + // Most of the methods are only here for completeness of the interface and are not actually used. + struct CompositeAvailablePackage : public IPackage + { + CompositeAvailablePackage() {} + CompositeAvailablePackage(std::shared_ptr<IPackage> availablePackage, std::optional<Pinning::Pin> pin = {}) + : m_availablePackage(availablePackage), m_pin(pin) + { + auto latestAvailable = m_availablePackage->GetLatestAvailableVersion(PinBehavior::IgnorePins); + if (latestAvailable) + { + m_sourceId = latestAvailable->GetSource().GetIdentifier(); + } + } + + const std::string& GetSourceId() const + { + return m_sourceId; + } + + const std::shared_ptr<IPackage>& GetAvailablePackage() const + { + return m_availablePackage; + } + + const std::optional<Pinning::Pin>& GetPin() const + { + return m_pin; + } + + void SetPin(Pinning::Pin&& pin) + { + m_pin = std::move(pin); + } + + Utility::LocIndString GetProperty(PackageProperty property) const override + { + return m_availablePackage->GetProperty(property); + } + + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + return {}; + } + + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + auto result = m_availablePackage->GetAvailableVersionKeys(); + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning) && m_pin.has_value()) + { + for (auto& pvk : result) + { + if (m_pin->GetType() == Pinning::PinType::Blocking || + m_pin->GetType() == Pinning::PinType::Pinning || + (m_pin->GetType() == Pinning::PinType::Gating && !m_pin->GetGatedVersion().IsValidVersion(pvk.Version))) + { + pvk.PinnedState = m_pin->GetType(); + } + } + } + + return result; + } + + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + return GetAvailableVersionAndPin(versionKey).first; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const override + { + auto availableVersionKeys = GetAvailableVersionKeys(); + auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, pinBehavior); + if (!latestVersionKey) + { + return {}; + } + + return GetAvailableVersion(latestVersionKey.value()); + } + + virtual std::pair<std::shared_ptr<IPackageVersion>, Pinning::PinType> GetAvailableVersionAndPin(const PackageVersionKey& versionKey) const override + { + Pinning::PinType pinType = Pinning::PinType::Unknown; + + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning) && m_pin.has_value()) + { + // A gating pin behaves the same as no pin when the version fits the gated version + if (!(pinType == Pinning::PinType::Gating && m_pin->GetGatedVersion().IsValidVersion(versionKey.Version))) + { + pinType = m_pin->GetType(); + } + } + + return { m_availablePackage->GetAvailableVersion(versionKey), pinType }; + } + + bool IsUpdateAvailable(PinBehavior) const override + { + return false; + } + + bool IsSame(const IPackage* other) const override + { + const CompositeAvailablePackage* otherAvailable = dynamic_cast<const CompositeAvailablePackage*>(other); + + if (otherAvailable) + { + return + m_sourceId == otherAvailable->m_sourceId && + m_pin == otherAvailable->m_pin && + m_availablePackage->IsSame(otherAvailable->m_availablePackage.get()); + } + + return false; + } + + private: + std::string m_sourceId; + std::shared_ptr<IPackage> m_availablePackage; + std::optional<Pinning::Pin> m_pin; + }; + // A composite package for the CompositeSource. struct CompositePackage : public IPackage { CompositePackage(std::shared_ptr<IPackage> installedPackage, std::shared_ptr<IPackage> availablePackage = {}) : - m_installedPackage(std::move(installedPackage)), m_availablePackage(std::move(availablePackage)) + m_installedPackage(std::move(installedPackage)) { // Grab the installed version's channel to allow for filtering in calls to get available info. if (m_installedPackage) @@ -336,12 +508,16 @@ namespace AppInstaller::Repository } } - TrySetOverrideInstalledVersion(); + AddAvailablePackage(std::move(availablePackage)); } Utility::LocIndString GetProperty(PackageProperty property) const override { - std::shared_ptr<IPackageVersion> truth = GetLatestAvailableVersion(); + std::shared_ptr<IPackageVersion> truth; + if (m_defaultAvailablePackage) + { + truth = m_defaultAvailablePackage->GetLatestAvailableVersion(PinBehavior::IgnorePins); + } if (!truth) { truth = m_trackingPackageVersion; @@ -350,6 +526,10 @@ namespace AppInstaller::Repository { truth = GetInstalledVersion(); } + if (!truth) + { + truth = GetLatestAvailableVersion(PinBehavior::IgnorePins); + } switch (property) { @@ -378,38 +558,63 @@ namespace AppInstaller::Repository std::vector<PackageVersionKey> GetAvailableVersionKeys() const override { - if (m_availablePackage) + std::vector<PackageVersionKey> result; + + for (const auto& availablePackage : m_availablePackages) { - std::vector<PackageVersionKey> result = m_availablePackage->GetAvailableVersionKeys(); - std::string_view channel = m_installedChannel; + auto versionKeys = availablePackage.GetAvailableVersionKeys(); + std::copy(versionKeys.begin(), versionKeys.end(), std::back_inserter(result)); + } - // Remove all elements whose channel does not match the installed package. - result.erase( - std::remove_if(result.begin(), result.end(), [&](const PackageVersionKey& pvk) { return !Utility::ICUCaseInsensitiveEquals(pvk.Channel, channel); }), - result.end()); + // Remove all elements whose channel does not match the installed package. + std::string_view channel = m_installedChannel; + result.erase( + std::remove_if(result.begin(), result.end(), [&](const PackageVersionKey& pvk) { return !Utility::ICUCaseInsensitiveEquals(pvk.Channel, channel); }), + result.end()); - return result; - } + // Put latest versions at the front; for versions available from multiple sources maintain the order they were added in + std::stable_sort(result.begin(), result.end()); - return {}; + return result; } - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const override { - return GetAvailableVersion({ "", "", m_installedChannel.get() }); + auto availableVersionKeys = GetAvailableVersionKeys(); + auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, pinBehavior); + if (!latestVersionKey) + { + return {}; + } + + return GetAvailableVersion(latestVersionKey.value()); } std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override { - if (m_availablePackage) + return GetAvailableVersionAndPin(versionKey).first; + } + + std::pair<std::shared_ptr<IPackageVersion>, Pinning::PinType> GetAvailableVersionAndPin(const PackageVersionKey& versionKey) const override + { + for (const auto& availablePackage : m_availablePackages) { - return m_availablePackage->GetAvailableVersion(versionKey); + if (!Utility::IsEmptyOrWhitespace(versionKey.SourceId) && versionKey.SourceId != availablePackage.GetSourceId()) + { + continue; + } + + auto result = availablePackage.GetAvailableVersionAndPin(versionKey); + if (result.first) + { + return result; + } } return {}; } - bool IsUpdateAvailable() const override + bool IsUpdateAvailable(PinBehavior pinBehavior) const override { auto installed = GetInstalledVersion(); @@ -418,7 +623,7 @@ namespace AppInstaller::Repository return false; } - auto latest = GetLatestAvailableVersion(); + auto latest = GetLatestAvailableVersion(pinBehavior); return (latest && (GetVACFromVersion(installed.get()).IsUpdatedBy(GetVACFromVersion(latest.get())))); } @@ -430,23 +635,42 @@ namespace AppInstaller::Repository if (!otherComposite || static_cast<bool>(m_installedPackage) != static_cast<bool>(otherComposite->m_installedPackage) || (m_installedPackage && !m_installedPackage->IsSame(otherComposite->m_installedPackage.get())) || - static_cast<bool>(m_availablePackage) != static_cast<bool>(otherComposite->m_availablePackage) || - (m_availablePackage && !m_availablePackage->IsSame(otherComposite->m_availablePackage.get()))) + m_availablePackages.size() != otherComposite->m_availablePackages.size()) { return false; } + for (size_t i = 0; i < m_availablePackages.size(); ++i) + { + if (m_availablePackages[i].GetSourceId() != otherComposite->m_availablePackages[i].GetSourceId() || + !m_availablePackages[i].GetAvailablePackage()->IsSame(otherComposite->m_availablePackages[i].GetAvailablePackage().get())) + { + return false; + } + } + return true; } - const std::shared_ptr<IPackage>& GetInstalledPackage() const + bool IsSameAsAnyAvailable(const IPackage* other) const { - return m_installedPackage; + if (other) + { + for (const auto& availablePackage : m_availablePackages) + { + if (other->IsSame(availablePackage.GetAvailablePackage().get())) + { + return true; + } + } + } + + return false; } - const std::shared_ptr<IPackage>& GetAvailablePackage() const + const std::shared_ptr<IPackage>& GetInstalledPackage() const { - return m_availablePackage; + return m_installedPackage; } const std::shared_ptr<IPackage>& GetTrackingPackage() const @@ -454,10 +678,19 @@ namespace AppInstaller::Repository return m_trackingPackage; } - void SetAvailablePackage(std::shared_ptr<IPackage> availablePackage) + void AddAvailablePackage(std::shared_ptr<IPackage> availablePackage) { - m_availablePackage = std::move(availablePackage); - TrySetOverrideInstalledVersion(); + if (availablePackage) + { + if (!m_defaultAvailablePackage) + { + // Set override only with the first available version found + m_defaultAvailablePackage = availablePackage; + TrySetOverrideInstalledVersion(m_defaultAvailablePackage); + } + + m_availablePackages.emplace_back(std::move(availablePackage)); + } } void SetTracking(Source trackingSource, std::shared_ptr<IPackage> trackingPackage, std::shared_ptr<IPackageVersion> trackingPackageVersion) @@ -467,10 +700,34 @@ namespace AppInstaller::Repository m_trackingPackageVersion = std::move(trackingPackageVersion); } + // Gets the information about the pins that exist for this package + void GetExistingPins(PinningIndex& pinningIndex, bool cleanUpStalePins) + { + // If the package is installed, we need to add the pin information to the available packages from any source. + // If the package is not installed, we clean up stale pin information here. + for (auto& availablePackage : m_availablePackages) + { + auto pinKey = GetPinKey(availablePackage.GetAvailablePackage().get()); + if (m_installedPackage) + { + auto pin = pinningIndex.GetPin(pinKey); + if (pin.has_value()) + { + availablePackage.SetPin(std::move(pin.value())); + } + } + else if (pinningIndex.GetPin(pinKey) && cleanUpStalePins) + { + pinningIndex.RemovePin(pinKey); + } + } + } + private: - void TrySetOverrideInstalledVersion() + // Try to set a version that will override the version string from the installed package + void TrySetOverrideInstalledVersion(std::shared_ptr<IPackage> availablePackage) { - if (m_installedPackage && m_availablePackage) + if (m_installedPackage && availablePackage) { auto installedVersion = m_installedPackage->GetInstalledVersion(); if (installedVersion) @@ -478,7 +735,7 @@ namespace AppInstaller::Repository auto installedType = Manifest::ConvertToInstallerTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); if (Manifest::DoesInstallerTypeSupportArpVersionRange(installedType)) { - m_overrideInstalledVersion = GetMappedInstalledVersion(installedVersion->GetProperty(PackageVersionProperty::Version), m_availablePackage); + m_overrideInstalledVersion = GetMappedInstalledVersion(installedVersion->GetProperty(PackageVersionProperty::Version), availablePackage); } } } @@ -486,11 +743,12 @@ namespace AppInstaller::Repository std::shared_ptr<IPackage> m_installedPackage; Utility::LocIndString m_installedChannel; - std::shared_ptr<IPackage> m_availablePackage; Source m_trackingSource; std::shared_ptr<IPackage> m_trackingPackage; std::shared_ptr<IPackageVersion> m_trackingPackageVersion; std::string m_overrideInstalledVersion; + std::shared_ptr<IPackage> m_defaultAvailablePackage; + std::vector<CompositeAvailablePackage> m_availablePackages; }; // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. @@ -620,8 +878,8 @@ namespace AppInstaller::Repository { for (auto& match : Matches) { - const std::shared_ptr<IPackage>& availablePackage = match.Package->GetAvailablePackage(); - if (availablePackage && availablePackage->IsSame(availableMatch.Package.get())) + const CompositePackage* compositeMatch = dynamic_cast<const CompositePackage*>(match.Package.get()); + if (compositeMatch && compositeMatch->IsSameAsAnyAvailable(availableMatch.Package.get())) { if (ResultMatchComparator{}(availableMatch, match)) { @@ -831,6 +1089,25 @@ namespace AppInstaller::Repository return {}; } + + // Adds all the pin information to the results from a search to a CompositeSource. + // This function assumes that the CompositeSource included an InstalledSource so that we + // can clean up stale pins where the package is no longer installed. + void AddPinInfoToCompositeSearchResult(CompositeResult& result) + { + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning) && !result.Matches.empty()) + { + // Look up any pins for the packages found + auto pinningIndex = PinningIndex::OpenOrCreateDefault(); + if (pinningIndex) + { + for (auto& match : result.Matches) + { + match.Package->GetExistingPins(*pinningIndex, /* cleanUpStalePins */ true); + } + } + } + } } CompositeSource::CompositeSource(std::string identifier) @@ -950,7 +1227,6 @@ namespace AppInstaller::Repository std::shared_ptr<IPackage> trackingPackage; std::shared_ptr<IPackageVersion> trackingPackageVersion; std::chrono::system_clock::time_point trackingPackageTime; - std::shared_ptr<IPackage> availablePackage; // Check the tracking catalog first to see if there is a correlation there. // TODO: When the issue with support for multiple available packages is fixed, this should move into @@ -984,46 +1260,52 @@ namespace AppInstaller::Repository } } + bool addedAvailablePackage = false; + // Directly search for the available package from tracking information. if (trackingPackage) { - availablePackage = GetTrackedPackageFromAvailableSource(result, trackedSource, trackingPackage->GetProperty(PackageProperty::Id)); + addedAvailablePackage = true; + compositePackage->AddAvailablePackage(GetTrackedPackageFromAvailableSource(result, trackedSource, trackingPackage->GetProperty(PackageProperty::Id))); + compositePackage->SetTracking(std::move(trackedSource), std::move(trackingPackage), std::move(trackingPackageVersion)); } - if (!availablePackage) + // Search sources and add to result + for (const auto& source : m_availableSources) { - // Search sources and add to result - for (const auto& source : m_availableSources) + if (addedAvailablePackage && !ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning)) { - // Do not attempt to correlate local packages against this source - if (!source.GetDetails().SupportInstalledSearchCorrelation) - { - continue; - } - - SearchResult availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); + // Having multiple available packages is a new behavior introduced for package pinning, + // so we gate it with the same feature in case it causes problems. + break; + } - if (availableResult.Matches.empty()) - { - continue; - } + // Do not attempt to correlate local packages against this source + if (!source.GetDetails().SupportInstalledSearchCorrelation) + { + continue; + } - availablePackage = GetMatchingPackage(availableResult.Matches, - [&]() { - AICLI_LOG(Repo, Info, - << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << - "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate available package could not be determined"); - }); + SearchResult availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); - // We found some matching packages here, don't keep going - break; + if (availableResult.Matches.empty()) + { + continue; } - } - compositePackage->SetAvailablePackage(std::move(availablePackage)); - compositePackage->SetTracking(std::move(trackedSource), std::move(trackingPackage), std::move(trackingPackageVersion)); + // We will keep matching packages found from all sources, but generally we will use only the first one. + auto availablePackage = GetMatchingPackage(availableResult.Matches, + [&]() { + AICLI_LOG(Repo, Info, + << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << + "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); + }, [&] { + AICLI_LOG(Repo, Warning, << " Appropriate available package could not be determined"); + }); + + addedAvailablePackage = true; + compositePackage->AddAvailablePackage(std::move(availablePackage)); + } } // Move the installed result into the composite result @@ -1033,6 +1315,7 @@ namespace AppInstaller::Repository // Optimization for the "everything installed" case, no need to allow for reverse correlations if (request.IsForEverything() && m_searchBehavior == CompositeSearchBehavior::Installed) { + AddPinInfoToCompositeSearchResult(result); return std::move(result); } } @@ -1146,6 +1429,7 @@ namespace AppInstaller::Repository result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); } + AddPinInfoToCompositeSearchResult(result); return std::move(result); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -26,9 +26,25 @@ namespace AppInstaller::Repository::Microsoft return result; } +#ifndef AICLI_DISABLE_TEST_HOOKS + std::optional<std::filesystem::path> s_PinningIndexOverride{}; + void TestHook_SetPinningIndex_Override(std::optional<std::filesystem::path>&& indexPath) + { + s_PinningIndexOverride = std::move(indexPath); + } +#endif + std::shared_ptr<PinningIndex> PinningIndex::OpenOrCreateDefault(OpenDisposition openDisposition) { - auto indexPath = Runtime::GetPathTo(Runtime::PathName::LocalState) / "pinning.db"; + const auto DefaultIndexPath = Runtime::GetPathTo(Runtime::PathName::LocalState) / "pinning.db"; +#ifndef AICLI_DISABLE_TEST_HOOKS + const auto indexPath = s_PinningIndexOverride.has_value() ? s_PinningIndexOverride.value() : DefaultIndexPath; +#else + const auto indexPath = DefaultIndexPath; +#endif + + AICLI_LOG(Repo, Info, << "Opening pinning index"); + try { @@ -89,6 +105,19 @@ namespace AppInstaller::Repository::Microsoft return result; } + void PinningIndex::AddOrUpdatePin(const Pinning::Pin& pin) + { + auto existingPin = GetPin(pin.GetKey()); + if (existingPin.has_value()) + { + UpdatePin(pin); + } + else + { + AddPin(pin); + } + } + void PinningIndex::RemovePin(const Pinning::PinKey& pinKey) { std::lock_guard<std::mutex> lockInterface{ *m_interfaceLock }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -37,9 +37,13 @@ namespace AppInstaller::Repository::Microsoft // Adds a pin to the index. IdType AddPin(const Pinning::Pin& pin); - // Updates a pin type, and gated version if needed + // Updates a pin type, and gated version if needed. + // Return value indicates whether there were any changes. bool UpdatePin(const Pinning::Pin& pin); + // Adds a pin or updates it if it already exists. + void AddOrUpdatePin(const Pinning::Pin& pin); + // Removes a pin from the index. void RemovePin(const Pinning::PinKey& pinKey); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -252,7 +252,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior) const override { return GetLatestVersionInternal(); } @@ -277,7 +277,7 @@ namespace AppInstaller::Repository::Microsoft return {}; } - bool IsUpdateAvailable() const override + bool IsUpdateAvailable(PinBehavior) const override { return false; } @@ -316,7 +316,7 @@ namespace AppInstaller::Repository::Microsoft return {}; } - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior) const override { return {}; } @@ -326,7 +326,7 @@ namespace AppInstaller::Repository::Microsoft return {}; } - bool IsUpdateAvailable() const override + bool IsUpdateAvailable(PinBehavior) const override { return false; } diff --git a/src/AppInstallerRepositoryCore/PackageInstalledStatus.cpp b/src/AppInstallerRepositoryCore/PackageInstalledStatus.cpp @@ -139,7 +139,7 @@ namespace AppInstaller::Repository { // No installed version, or installed version not found in available versions, // then attempt to check installed status using latest version. - availableVersion = package->GetLatestAvailableVersion(); + availableVersion = package->GetLatestAvailableVersion(PinBehavior::IgnorePins); THROW_HR_IF(E_UNEXPECTED, !availableVersion); } diff --git a/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp b/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "winget/PackageTrackingCatalog.h" #include "PackageTrackingCatalogSourceFactory.h" +#include "winget/Pin.h" #include "winget/RepositorySource.h" #include "Microsoft/SQLiteIndexSource.h" #include "AppInstallerDateTime.h" @@ -238,7 +239,7 @@ namespace AppInstaller::Repository if (installer.RequireExplicitUpgrade) { - index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::PinnedState, ToString(PackagePinnedState::PinnedByManifest)); + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::PinnedState, ToString(Pinning::PinType::PinnedByManifest)); } // Record installed architecture and locale if applicable diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -6,6 +6,7 @@ #include <AppInstallerVersions.h> #include <winget/LocIndependent.h> #include <winget/Manifest.h> +#include <winget/Pin.h> #include <map> #include <memory> @@ -181,7 +182,8 @@ namespace AppInstaller::Repository TrackingWriteTime, // The Architecture of an installed package InstalledArchitecture, - // The PackagePinnedState of the installed package + // The pinned state of the installed package + // As a package can have multiple pins for multiple sources, this is the strictest pin PinnedState, // The Architecture of user intent UserIntentArchitecture, @@ -192,17 +194,6 @@ namespace AppInstaller::Repository // Convert a PackageVersionMetadata to a string. std::string_view ToString(PackageVersionMetadata pvm); - // Possible pinned states for a package. - // Pinned packages need to be explicitly updated (i.e., are not included in `upgrade --all`) - enum class PackagePinnedState - { - NotPinned, - PinnedByManifest, - }; - - std::string_view ToString(PackagePinnedState state); - PackagePinnedState ConvertToPackagePinnedStateEnum(std::string_view in); - // A single package version. struct IPackageVersion { @@ -232,8 +223,8 @@ namespace AppInstaller::Repository { PackageVersionKey() = default; - PackageVersionKey(Utility::NormalizedString sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : - SourceId(std::move(sourceId)), Version(std::move(version)), Channel(std::move(channel)) {} + PackageVersionKey(Utility::NormalizedString sourceId, Utility::NormalizedString version, Utility::NormalizedString channel, Pinning::PinType pinnedState = Pinning::PinType::Unknown) : + SourceId(std::move(sourceId)), Version(std::move(version)), Channel(std::move(channel)), PinnedState(pinnedState) {} // The source id that this version came from. std::string SourceId; @@ -243,8 +234,20 @@ namespace AppInstaller::Repository // The channel. Utility::NormalizedString Channel; + + // The pin state for this package version, if it came from a list of available versions. + // When used to look up a package version, this field is not considered. + Pinning::PinType PinnedState = Pinning::PinType::Unknown; + + bool operator<(const PackageVersionKey& other) const + { + // Sort using only the version and channel. + // The order for the sources depends on the context. + return Utility::VersionAndChannel({ Version }, { Channel }) < Utility::VersionAndChannel({ other.Version }, { other.Channel }); + } }; + // A property of a package. enum class PackageProperty { @@ -300,6 +303,18 @@ namespace AppInstaller::Repository std::vector<InstalledStatus> Status; }; + // Possible ways to consider pins when getting a package's available versions + enum class PinBehavior + { + // Ignore pins, returns all available versions. + IgnorePins, + // Include available versions for packages with a Pinning pin. + // Blocking pins and Gating pins still respected. + IncludePinned, + // Respect all the types of pins. + ConsiderPins, + }; + // A package, potentially containing information about it's local state and the available versions. struct IPackage { @@ -311,19 +326,30 @@ namespace AppInstaller::Repository // Gets the installed package information. virtual std::shared_ptr<IPackageVersion> GetInstalledVersion() const = 0; + // Note on pins: + // Pins only make sense when there is both an installed and an available version. + // Only for the composite source will GetAvailableVersionKeys() include pinned state, + // and GetLatestAvailableVersion() consider the pin behavior. + // Gets all available versions of this package. // The versions will be returned in sorted, descending order. // Ex. { 4, 3, 2, 1 } + // The list may contain versions from multiple sources. virtual std::vector<PackageVersionKey> GetAvailableVersionKeys() const = 0; // Gets a specific version of this package. - virtual std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const = 0; + virtual std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const = 0; // Gets a specific version of this package. virtual std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const = 0; + virtual std::pair<std::shared_ptr<IPackageVersion>, Pinning::PinType> GetAvailableVersionAndPin(const PackageVersionKey& versionKey) const + { + return { GetAvailableVersion(versionKey), Pinning::PinType::Unknown }; + } + // Gets a value indicating whether an available version is newer than the installed version. - virtual bool IsUpdateAvailable() const = 0; + virtual bool IsUpdateAvailable(PinBehavior pinBehavior) const = 0; // Determines if the given IPackage refers to the same package as this one. virtual bool IsSame(const IPackage*) const = 0; diff --git a/src/AppInstallerRepositoryCore/RepositorySearch.cpp b/src/AppInstallerRepositoryCore/RepositorySearch.cpp @@ -92,30 +92,6 @@ namespace AppInstaller::Repository } } - std::string_view ToString(PackagePinnedState state) - { - switch (state) - { - case PackagePinnedState::PinnedByManifest: return "PinnedByManifest"sv; - case PackagePinnedState::NotPinned: - default: - return "Unknown"; - } - } - - PackagePinnedState ConvertToPackagePinnedStateEnum(std::string_view in) - { - if (Utility::CaseInsensitiveEquals(in, "PinnedByManifest"sv)) - { - return PackagePinnedState::PinnedByManifest; - } - else - { - return PackagePinnedState::NotPinned; - } - } - - const char* UnsupportedRequestException::what() const noexcept { if (m_whatMessage.empty()) diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -72,7 +72,7 @@ namespace AppInstaller::Repository::Rest return result; } - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior) const override { std::scoped_lock versionsLock{ m_packageVersionsLock }; return GetLatestVersionInternal(); @@ -80,7 +80,7 @@ namespace AppInstaller::Repository::Rest std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override; - bool IsUpdateAvailable() const override + bool IsUpdateAvailable(PinBehavior) const override { return false; } diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -220,6 +220,8 @@ namespace AppInstaller return "One or more applications failed to uninstall"; case APPINSTALLER_CLI_ERROR_NOT_ALL_QUERIES_FOUND_SINGLE: return "One or more queries did not return exactly one match"; + case APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED: + return "The package has a pin that prevents upgrade."; // Install errors case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -116,6 +116,7 @@ #define APPINSTALLER_CLI_ERROR_MULTIPLE_INSTALL_FAILED ((HRESULT)0x8A150065) #define APPINSTALLER_CLI_ERROR_MULTIPLE_UNINSTALL_FAILED ((HRESULT)0x8A150066) #define APPINSTALLER_CLI_ERROR_NOT_ALL_QUERIES_FOUND_SINGLE ((HRESULT)0x8A150067) +#define APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED ((HRESULT)0x8A150068) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/Microsoft.Management.Deployment/CatalogPackage.cpp b/src/Microsoft.Management.Deployment/CatalogPackage.cpp @@ -66,7 +66,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::call_once(m_defaultInstallVersionOnceFlag, [&]() { - std::shared_ptr<::AppInstaller::Repository::IPackageVersion> latestVersion = m_package.get()->GetLatestAvailableVersion(); + std::shared_ptr<::AppInstaller::Repository::IPackageVersion> latestVersion = m_package.get()->GetLatestAvailableVersion(AppInstaller::Repository::PinBehavior::IgnorePins); if (latestVersion) { // DefaultInstallVersion hasn't been created yet, create and populate it. @@ -96,7 +96,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation } bool CatalogPackage::IsUpdateAvailable() { - return m_package->IsUpdateAvailable(); + return m_package->IsUpdateAvailable(AppInstaller::Repository::PinBehavior::IgnorePins); } Windows::Foundation::IAsyncOperation<winrt::Microsoft::Management::Deployment::CheckInstalledStatusResult> CatalogPackage::CheckInstalledStatusAsync( Microsoft::Management::Deployment::InstalledStatusType checkTypes)