diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index a5f15c9ca6..5a83c6e050 100644 --- 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 index b8530a2476..0cd5088785 100644 --- 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 index 2702ed4688..592a254b63 100644 --- 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 index cc66d9f98a..216bed615b 100644 --- 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 index 155ba8594e..753a0a3985 100644 --- 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 index dfff48779e..d65e56c8f6 100644 --- 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 index 1770bd22e1..42aef1428b 100644 --- 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 index 55f2294348..9020e354c3 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -57,17 +57,27 @@ namespace AppInstaller::CLI::Workflow { auto package = context.Get(); std::vector pins; - - // TODO: We should support querying the multiple sources for a package, instead of just one - auto availableVersion = package->GetLatestAvailableVersion(); + std::set sources; auto pinningIndex = context.Get(); - 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(std::move(pins)); @@ -78,49 +88,73 @@ namespace AppInstaller::CLI::Workflow auto package = context.Get(); auto installedVersion = context.Get(); - std::vector 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 pinsToAddOrUpdate; + std::set sources; auto pinningIndex = context.Get(); - 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(); std::vector pins; - - // TODO: We should support querying the multiple sources for a package, instead of just one - auto availableVersion = package->GetLatestAvailableVersion(); + std::set sources; auto pinningIndex = context.Get(); - 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 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(); std::vector 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(std::move(matchingPins)); } diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h index fd4a5fce2c..040ea5116e 100644 --- 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 index e0e6536c07..49cbe66b44 100644 --- 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 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(); auto installedPackage = context.Get(); + 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()->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 index 3339a43c2c..2f149009f6 100644 --- 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 index 9d0e2797f4..e18c9f2d3a 100644 --- 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 #include +#include +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(); 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()->GetAvailableVersion(key); + + std::shared_ptr package = context.Get(); + std::shared_ptr 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; if (requestedVersion) @@ -912,9 +1007,12 @@ namespace AppInstaller::CLI::Workflow context.Add(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.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 index bf28cfe0f6..e88a5f72c5 100644 --- 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 index a2d5acb3d2..99209261b0 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -229,6 +229,7 @@ public class ErrorCode 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 new file mode 100644 index 0000000000..ae65a4a942 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/Pinning.cs @@ -0,0 +1,246 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using NUnit.Framework; + using static AppInstallerCLIE2ETests.TestCommon; + + /// + /// Test upgrading pinned packages. + /// + public class Pinning : BaseCommand + { + /// + /// Setup done once before all the tests here. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("pinning", true); + } + + /// + /// Set up for all tests. + /// + [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"); + } + + /// + /// Clean up done after all the tests here. + /// + [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 + + /// + /// Tests upgrading a package when there are no pins on it. + /// + [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"); + } + + /// + /// Tests upgrading a package when it has a pinning pin. + /// + [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")); + } + + /// + /// Tests upgrading a package when it has a gating pin that allows updating to another version. + /// + [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")); + } + + /// + /// Tests upgrading a package when it has a gating pin that blocks all other versions. + /// + [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"); + } + + /// + /// Tests upgrading a package when it has a blocking pin. + /// + [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"); + } + + /// + /// Tests upgrading a package when it has a pinning pin and the --force flag is used. + /// + [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"); + } + + /// + /// Tests upgrading a package when it has a gating pin and the --force flag is used. + /// + [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")); + } + + /// + /// Tests upgrading a package when it has a blocking pin and the --force flag is used. + /// + [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 index fd07462c6f..423f51e59c 100644 --- a/src/AppInstallerCLIE2ETests/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -564,7 +564,7 @@ public static string GetTestServerCertificateHexString() /// Install directory. /// Optional expected content. /// True if success. - 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 @@ public static bool VerifyTestExeInstalledAndCleanup(string installDir, string ex verifyInstallSuccess = content.Contains(expectedContent); } + return verifyInstallSuccess; + } + + /// + /// Verify exe installer correctly and then uninstall it. + /// + /// Install directory. + /// Optional expected content. + /// True if success. + 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 new file mode 100644 index 0000000000..073e3c5c71 --- /dev/null +++ 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: + 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 + InstallLocation: /InstallDir +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 new file mode 100644 index 0000000000..2cf660aa43 --- /dev/null +++ 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: + 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 + InstallLocation: /InstallDir +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml index 506812f7d6..9e71098bc3 100644 --- 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: 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 index 6a62959ba1..8277e1f45f 100644 --- 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: 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 index 0f564b19ad..65574991d2 100644 --- a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs +++ b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -81,13 +81,27 @@ public static void DeleteDirectoryContents(DirectoryInfo directory) // 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 index 18c7ba8888..e0b83e4657 100644 --- 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. This package's version number cannot be determined. To upgrade it anyway, add the argument --include-unknown to your previous command. {Locked="--include-unknown"} - - packages have version numbers that cannot be determined. Using --include-unknown may show more results. - {Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. - - - package has a version number that cannot be determined. Using --include-unknown may show more results. - {Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. - - package(s) have version numbers that cannot be determined. Use --include-unknown to see all results. - {Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. + {0} package(s) have version numbers that cannot be determined. Use --include-unknown to see all results. + {Locked="{0}","--include-unknown"} {0} is a placeholder that is replaced by an integer number of packages that do not have notated versions. - package(s) is pinned and needs to be explicitly upgraded. - This string is preceded by a (integer) number of packages that require explicit upgrades. + {0} package(s) are pinned and need to be explicitly upgraded. + {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages that require explicit upgrades. The arguments provided can only be used with a query. @@ -1536,14 +1528,14 @@ Please specify one of them using the --source option to proceed. {Locked="{0}"} The value will be replaced with the feature name - 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'. + 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'. {Locked{"'upgrade'"} Locked{"--include-pinned"} Locked{"winget upgrade --all"} Add a new pin - 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. + 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. Manage package pins @@ -1570,7 +1562,7 @@ Please specify one of them using the --source option to proceed. Version to which to pin the package. The wildcard '*' can be used as the last version part - Block from updating until the pin is removed, preventing override arguments + Block from upgrading until the pin is removed, preventing override arguments Export settings as JSON @@ -1593,15 +1585,16 @@ Please specify one of them using the --source option to proceed. Pin added successfully - The pin already exists + There is already a pin for package {0} + {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. - A pin already exists for this package. Overwriting due to the --force argument. - {Locked="--force"} + A pin already exists for package {0}. Overwriting due to the --force argument. + {Locked="--force"}{Locked="--force","{0}"} {0} is a placeholder that will be replaced by a package name. - A pin already exists for this package. Use the --force argument to overwrite it. - {Locked="--force"} + A pin already exists for package {0}. Use the --force argument to overwrite it. + {Locked="--force","{0}"} {0} is a placeholder that will be replaced by a package name. Resetting all current pins. @@ -1663,4 +1656,25 @@ Please specify one of them using the --source option to proceed. Search failed for: {0} {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. + + {0} package(s) have a blocking pin that needs to be removed before upgrade + {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with blocking pins + + + Upgrade packages even if they have a non-blocking pin + + + Pin removed successfully + + + A newer version was found, but the package has a pin that prevents from upgrading it. + + + 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. + {Locked="winget pin","--include-pinned"} Error shown when we block an upgrade due to the package being pinned + + + {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. + {Locked="winget pin","--include-pinned"} {0} is a placeholder replaced by an integer number of packages + \ No newline at end of file diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp index 5a42f87705..629e6f8300 100644 --- a/src/AppInstallerCLITests/CompositeSource.cpp +++ b/src/AppInstallerCLITests/CompositeSource.cpp @@ -6,12 +6,15 @@ #include "TestHooks.h" #include #include +#include #include +#include 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("Name"); result.DefaultLocalization.Add("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 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(), m_source); } else { - m_package = TestPackage::Make(std::vector{ m_manifest }); + m_package = TestPackage::Make(std::vector{ m_manifest }, m_source); } } @@ -167,17 +171,18 @@ struct TestPackageHelper private: bool m_isInstalled; Manifest::Manifest m_manifest; + std::shared_ptr m_source; std::shared_ptr m_package; }; TestPackageHelper MakeInstalled() { - return { true }; + return { /* isInstalled */ true}; } -TestPackageHelper MakeAvailable() +TestPackageHelper MakeAvailable(std::shared_ptr source) { - return { false }; + return { /* isInstalled */ false, source}; } void RequireIncludes(const std::vector& filters, PackageMatchField field, MatchType type, std::optional 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{ noChannel, hasChannel }), Criteria()); + result.Matches.emplace_back(TestPackage::Make(std::vector{ 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{ noChannel, hasChannel }), Criteria()); + result.Matches.emplace_back(TestPackage::Make(std::vector{ 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(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 AvailableFails = std::make_shared(); 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 latestAvailableVersion) + : IsUpdateAvailable(isUpdateAvailable), LatestAvailableVersion(latestAvailableVersion) {} + ExpectedResultForPinBehavior() {} + + bool IsUpdateAvailable = false; + std::optional LatestAvailableVersion; +}; + +struct ExpectedResultsForPinning +{ + std::map ResultsForPinBehavior; + std::vector AvailableVersions; +}; + +void RequireExpectedResultsWithPin(std::shared_ptr 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(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{ 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(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{ MakeDefaultManifest("2.0"sv) }, setup.Available); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + std::shared_ptr secondAvailable = std::make_shared("SecondTestSource"); + setup.Composite.AddAvailableSource(Source{ secondAvailable }); + secondAvailable->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make(std::vector{ 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(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{ + MakeDefaultManifest("2.0"sv), + MakeDefaultManifest("1.2"sv), + }, + setup.Available); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + std::shared_ptr secondAvailable = std::make_shared("SecondTestSource"); + setup.Composite.AddAvailableSource(Source{ secondAvailable }); + secondAvailable->SearchFunction = [&](const SearchRequest&) + { + auto package = TestPackage::Make(std::vector{ 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 index 1b9f0dc636..98b3c2767e 100644 --- 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()); // 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 index 42989fc5da..a3cc70a285 100644 --- 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 #include #include #include +#include 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 index b22e97c762..c0216e204d 100644 --- 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 index 6e8626c64b..3c7dda48bd 100644 --- 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()); } diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index ee0c56573d..0496d4a20f 100644 --- 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&& indexPath); + } + namespace Logging { void TestHook_SetTelemetryOverride(std::shared_ptr 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 index 11b382de0c..8aec0e4367 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -182,12 +182,12 @@ namespace TestCommon std::vector 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 TestPackage::GetLatestAvailableVersion() const + std::shared_ptr 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 index bcac291f47..af6a4fc9dd 100644 --- 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 GetInstalledVersion() const override; std::vector GetAvailableVersionKeys() const override; - std::shared_ptr GetLatestAvailableVersion() const override; + std::shared_ptr GetLatestAvailableVersion(AppInstaller::Repository::PinBehavior) const override; std::shared_ptr 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 InstalledVersion; diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp index 88abd06e9d..21ee489ea7 100644 --- 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 index 32a6b36d68..181db0fe3d 100644 --- 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 index d76d0f4ea4..429d572102 100644 --- 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 index 6b4a01cd0c..c6e1f25de2 100644 --- 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& 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 index 3a617e18ba..2382e24a47 100644 --- 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 '. // User has to unblock to allow update. Blocking, // The package is excluded from 'upgrade --all', unless '--include-pinned' is added. // 'upgrade ' 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 index a300e8cdcc..286a2cb9fb 100644 --- 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& ranges) { for (size_t i = 0; i < ranges.size(); i++) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 20ddecffbc..15ae93b791 100644 --- 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 + +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 GetLatestAvailableVersionKeySatisfyingPin(const std::vector& availableVersionKeys, PinBehavior pinBehavior) + { + if (availableVersionKeys.empty()) + { + return {}; + } + + std::optional 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& 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 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 availablePackage, std::optional 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& GetAvailablePackage() const + { + return m_availablePackage; + } + + const std::optional& 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 GetInstalledVersion() const override + { + return {}; + } + + std::vector 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 GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + return GetAvailableVersionAndPin(versionKey).first; + } + + std::shared_ptr GetLatestAvailableVersion(PinBehavior pinBehavior) const override + { + auto availableVersionKeys = GetAvailableVersionKeys(); + auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, pinBehavior); + if (!latestVersionKey) + { + return {}; + } + + return GetAvailableVersion(latestVersionKey.value()); + } + + virtual std::pair, 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(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 m_availablePackage; + std::optional m_pin; + }; + // A composite package for the CompositeSource. struct CompositePackage : public IPackage { CompositePackage(std::shared_ptr installedPackage, std::shared_ptr 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 truth = GetLatestAvailableVersion(); + std::shared_ptr 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 GetAvailableVersionKeys() const override { - if (m_availablePackage) + std::vector result; + + for (const auto& availablePackage : m_availablePackages) { - std::vector 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 GetLatestAvailableVersion() const override + std::shared_ptr 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 GetAvailableVersion(const PackageVersionKey& versionKey) const override { - if (m_availablePackage) + return GetAvailableVersionAndPin(versionKey).first; + } + + std::pair, 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(m_installedPackage) != static_cast(otherComposite->m_installedPackage) || (m_installedPackage && !m_installedPackage->IsSame(otherComposite->m_installedPackage.get())) || - static_cast(m_availablePackage) != static_cast(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& 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& GetAvailablePackage() const + const std::shared_ptr& GetInstalledPackage() const { - return m_availablePackage; + return m_installedPackage; } const std::shared_ptr& GetTrackingPackage() const @@ -454,10 +678,19 @@ namespace AppInstaller::Repository return m_trackingPackage; } - void SetAvailablePackage(std::shared_ptr availablePackage) + void AddAvailablePackage(std::shared_ptr 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 trackingPackage, std::shared_ptr 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 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 m_installedPackage; Utility::LocIndString m_installedChannel; - std::shared_ptr m_availablePackage; Source m_trackingSource; std::shared_ptr m_trackingPackage; std::shared_ptr m_trackingPackageVersion; std::string m_overrideInstalledVersion; + std::shared_ptr m_defaultAvailablePackage; + std::vector 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& availablePackage = match.Package->GetAvailablePackage(); - if (availablePackage && availablePackage->IsSame(availableMatch.Package.get())) + const CompositePackage* compositeMatch = dynamic_cast(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 trackingPackage; std::shared_ptr trackingPackageVersion; std::chrono::system_clock::time_point trackingPackageTime; - std::shared_ptr 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 index aa4688dc12..bb307b4a7e 100644 --- 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 s_PinningIndexOverride{}; + void TestHook_SetPinningIndex_Override(std::optional&& indexPath) + { + s_PinningIndexOverride = std::move(indexPath); + } +#endif + std::shared_ptr 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 lockInterface{ *m_interfaceLock }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index 77180331b5..5313d7ca54 100644 --- 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 index bb74dda174..58b737c851 100644 --- 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 GetLatestAvailableVersion() const override + std::shared_ptr 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 GetLatestAvailableVersion() const override + std::shared_ptr 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 index 2270db5415..bc242075c3 100644 --- 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 index 74fefec83d..504834dd79 100644 --- 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 index 319d851453..4ae48bc037 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -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 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 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 GetAvailableVersionKeys() const = 0; // Gets a specific version of this package. - virtual std::shared_ptr GetLatestAvailableVersion() const = 0; + virtual std::shared_ptr GetLatestAvailableVersion(PinBehavior pinBehavior) const = 0; // Gets a specific version of this package. virtual std::shared_ptr GetAvailableVersion(const PackageVersionKey& versionKey) const = 0; + virtual std::pair, 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 index 35eebc73d6..2384b3b893 100644 --- 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 index 46d2b04d68..2578b487fc 100644 --- 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 GetLatestAvailableVersion() const override + std::shared_ptr GetLatestAvailableVersion(PinBehavior) const override { std::scoped_lock versionsLock{ m_packageVersionsLock }; return GetLatestVersionInternal(); @@ -80,7 +80,7 @@ namespace AppInstaller::Repository::Rest std::shared_ptr 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 index be3120a484..01c0b060dc 100644 --- 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 index 97e096c3a8..2754648a4a 100644 --- 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 index 16abdd270b..8ae39a78d5 100644 --- 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 CatalogPackage::CheckInstalledStatusAsync( Microsoft::Management::Deployment::InstalledStatusType checkTypes)