winget-cli

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

commit edf4fbe68061b1a3a60f436f7549e5ef230dfd6a
parent fee1b43faa7beca8c4ac44a77c1e07c5379a21ab
Author: Flor Chacón <14323496+florelis@users.noreply.github.com>
Date:   Tue,  9 May 2023 14:48:02 -0700

Record Product Codes in pinning table (#3167)


Diffstat:
M.github/actions/spelling/allow.txt | 1+
Msrc/AppInstallerCLICore/Argument.cpp | 2++
Msrc/AppInstallerCLICore/Commands/PinCommand.cpp | 41+++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 3+++
Msrc/AppInstallerCLICore/Workflows/PinFlow.cpp | 235+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/AppInstallerCLICore/Workflows/PinFlow.h | 13+++++--------
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 11+++++++++++
Msrc/AppInstallerCLITests/CompositeSource.cpp | 192++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerCLITests/MsixManifest.cpp | 4++--
Msrc/AppInstallerCLITests/PinFlow.cpp | 6+++---
Msrc/AppInstallerCLITests/PinningIndex.cpp | 3+--
Msrc/AppInstallerCLITests/RestInterface_1_0.cpp | 2+-
Msrc/AppInstallerCLITests/RestInterface_1_1.cpp | 4++--
Msrc/AppInstallerCLITests/TestSource.cpp | 2+-
Msrc/AppInstallerCLITests/TestSource.h | 2+-
Msrc/AppInstallerCommonCore/Pin.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/AppInstallerCommonCore/Public/winget/Pin.h | 56+++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/AppInstallerRepositoryCore/CompositeSource.cpp | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/AppInstallerRepositoryCore/InstalledFilesCorrelation.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp | 8++++----
Msrc/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp | 4++--
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp | 31++++++++++++++++++++-----------
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp | 13++++++++++++-
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h | 2+-
Msrc/AppInstallerRepositoryCore/Rest/RestSource.cpp | 2+-
29 files changed, 648 insertions(+), 286 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -397,6 +397,7 @@ pfp PGP php PII +pinnable pinningindex pipssource PKCS diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -168,6 +168,8 @@ namespace AppInstaller::CLI return { type, "version"_liv, 'v', ArgTypeCategory::None, ArgTypeExclusiveSet::PinType }; case Execution::Args::Type::BlockingPin: return { type, "blocking"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PinType }; + case Execution::Args::Type::PinInstalled: + return { type, "installed"_liv, ArgTypeCategory::None }; // Configuration commands case Execution::Args::Type::ConfigurationFile: diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -62,6 +62,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::Force), Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag }, + Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, }; } @@ -113,16 +114,28 @@ namespace AppInstaller::CLI void PinAddCommand::ExecuteInternal(Execution::Context& context) const { + if (context.Args.Contains(Execution::Args::Type::Id)) + { + // When we are given an ID, just pin that available package without checking for installed. + // This helps when there are matching issues, for example due to multiple side-by-side installs. + context << + Workflow::OpenSource(); + } + else + { + // If not working from just ID, try matching a single installed package + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + } + context << - Workflow::OpenSource() << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(OperationType::Pin) << Workflow::GetInstalledPackageVersion << Workflow::ReportPackageIdentity << Workflow::OpenPinningIndex() << - Workflow::SearchPin << Workflow::AddPin; } @@ -139,6 +152,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), + Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, }; } @@ -185,9 +199,22 @@ namespace AppInstaller::CLI void PinRemoveCommand::ExecuteInternal(Execution::Context& context) const { + if (context.Args.Contains(Execution::Args::Type::Id)) + { + // When we are given an ID, just un-pin that available package without checking for installed. + // This helps when there are matching issues, for example due to multiple side-by-side installs. + context << + Workflow::OpenSource(); + } + else + { + // If not working from just ID, try matching a single installed package + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + } + context << - Workflow::OpenSource() << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(OperationType::Pin) << @@ -261,8 +288,7 @@ namespace AppInstaller::CLI Workflow::OpenPinningIndex(/* readOnly */ true) << Workflow::GetAllPins << Workflow::OpenSource() << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << - Workflow::CrossReferencePinsWithSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, false, Repository::CompositeSearchBehavior::AllPackages) << Workflow::ReportPins; } @@ -308,7 +334,6 @@ namespace AppInstaller::CLI Workflow::GetAllPins << Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << - Workflow::CrossReferencePinsWithSource << Workflow::ReportPins; } } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -98,6 +98,7 @@ namespace AppInstaller::CLI::Execution // Pin command GatedVersion, // Differs from Version in that this supports wildcards BlockingPin, + PinInstalled, // Configuration ConfigurationFile, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -318,6 +318,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg); + WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledSource); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist); @@ -330,6 +332,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll); WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); WINGET_DEFINE_RESOURCE_STRINGID(PinType); + WINGET_DEFINE_RESOURCE_STRINGID(PinVersion); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -16,21 +16,67 @@ namespace AppInstaller::CLI::Workflow namespace { // Creates a Pin appropriate for the context based on the arguments provided - Pinning::Pin CreatePin(Execution::Context& context, std::string_view packageId, std::string_view sourceId) + Pinning::Pin CreatePin(Execution::Context& context, const Pinning::PinKey& pinKey) { if (context.Args.Contains(Execution::Args::Type::GatedVersion)) { - return Pinning::Pin::CreateGatingPin({ packageId, sourceId }, context.Args.GetArg(Execution::Args::Type::GatedVersion)); + return Pinning::Pin::CreateGatingPin(pinKey, context.Args.GetArg(Execution::Args::Type::GatedVersion)); } else if (context.Args.Contains(Execution::Args::Type::BlockingPin)) { - return Pinning::Pin::CreateBlockingPin({ packageId, sourceId }); + return Pinning::Pin::CreateBlockingPin(pinKey); } else { - return Pinning::Pin::CreatePinningPin({ packageId, sourceId }); + return Pinning::Pin::CreatePinningPin(pinKey); } } + + std::set<Pinning::PinKey> GetPinKeysForPackage(Execution::Context& context) + { + auto package = context.Get<Execution::Data::Package>(); + + std::set<Pinning::PinKey> pinKeys; + + if (context.Args.Contains(Execution::Args::Type::PinInstalled)) + { + auto installedVersion = package->GetInstalledVersion(); + if (installedVersion) + { + pinKeys.emplace(Pinning::PinKey::GetPinKeyForInstalled(installedVersion->GetProperty(PackageVersionProperty::Id))); + } + } + else + { + auto packageVersionKeys = package->GetAvailableVersionKeys(); + for (const auto& versionKey : packageVersionKeys) + { + auto availableVersion = package->GetAvailableVersion(versionKey); + pinKeys.emplace( + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get()); + } + } + + return pinKeys; + } + + // Gets a search request that can be used to find the installed package that corresponds with a pin. + SearchRequest GetSearchRequestForPin(const Pinning::PinKey& pinKey) + { + SearchRequest searchRequest; + if (pinKey.IsForInstalled()) + { + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, pinKey.PackageId)); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, pinKey.PackageId)); + } + else + { + searchRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, pinKey.PackageId); + } + + return searchRequest; + } } void OpenPinningIndex::operator()(Execution::Context& context) const @@ -55,28 +101,18 @@ namespace AppInstaller::CLI::Workflow void SearchPin(Execution::Context& context) { - auto package = context.Get<Execution::Data::Package>(); - std::vector<Pinning::Pin> pins; - std::set<std::string> sources; + auto pinKeys = GetPinKeysForPackage(context); + auto package = context.Get<Execution::Data::Package>(); auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); - auto packageVersionKeys = package->GetAvailableVersionKeys(); - for (const auto& versionKey : packageVersionKeys) - + std::vector<Pinning::Pin> pins; + for (const auto& pinKey : pinKeys) { - 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) { - auto pin = pinningIndex->GetPin(pinKey); - if (pin) - { - pins.emplace_back(std::move(pin.value())); - } + pins.emplace_back(std::move(pin.value())); } } @@ -85,46 +121,41 @@ namespace AppInstaller::CLI::Workflow void AddPin(Execution::Context& context) { + auto pinKeys = GetPinKeysForPackage(context); + auto package = context.Get<Execution::Data::Package>(); + auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); auto installedVersion = context.Get<Execution::Data::InstalledPackageVersion>(); - auto installedVersionString = installedVersion->GetProperty(PackageVersionProperty::Version); - std::vector<Pinning::Pin> pinsToAddOrUpdate; - std::set<std::string> sources; - - auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); - - auto packageVersionKeys = package->GetAvailableVersionKeys(); - for (const auto& versionKey : packageVersionKeys) - + for (const auto& pinKey : pinKeys) { - auto availableVersion = package->GetAvailableVersion(versionKey); - Pinning::PinKey pinKey{ - availableVersion->GetProperty(PackageVersionProperty::Id).get(), - availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; - - if (!sources.insert(pinKey.SourceId).second) - { - // We already considered the pin for this source - continue; - } - - 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 pin = CreatePin(context, pinKey); + AICLI_LOG(CLI, Info, << "Evaluating Pin " << pin.ToString()); auto existingPin = pinningIndex->GetPin(pinKey); - if (existingPin) { - auto packageName = availableVersion->GetProperty(PackageVersionProperty::Name); + Utility::LocIndString packageNameToReport; + if (pinKey.IsForInstalled() && installedVersion) + { + packageNameToReport = installedVersion->GetProperty(PackageVersionProperty::Name); + } + else + { + auto availableVersion = package->GetAvailableVersion({ pinKey.SourceId, "", "" }); + if (availableVersion) + { + packageNameToReport = 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; + context.Reporter.Info() << Resource::String::PinAlreadyExists(packageNameToReport) << std::endl; continue; } @@ -132,12 +163,12 @@ namespace AppInstaller::CLI::Workflow 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; + context.Reporter.Warn() << Resource::String::PinExistsOverwriting(packageNameToReport) << std::endl; pinsToAddOrUpdate.push_back(std::move(pin)); } else { - context.Reporter.Error() << Resource::String::PinExistsUseForceArg(packageName) << std::endl; + context.Reporter.Error() << Resource::String::PinExistsUseForceArg(packageNameToReport) << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS); } } @@ -162,8 +193,7 @@ namespace AppInstaller::CLI::Workflow void RemovePin(Execution::Context& context) { auto package = context.Get<Execution::Data::Package>(); - std::vector<Pinning::Pin> pins; - std::set<std::string> sources; + auto pins = context.Get<Execution::Data::Pins>(); auto pinningIndex = context.Get<Execution::Data::PinningIndex>(); bool pinExists = false; @@ -172,23 +202,12 @@ namespace AppInstaller::CLI::Workflow // 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) + for (const auto& pin : pins) { - 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; - } - } + AICLI_LOG(CLI, Info, << "Removing Pin " << pin.GetKey().ToString()); + pinningIndex->RemovePin(pin.GetKey()); + pinExists = true; } if (!pinExists) @@ -210,29 +229,59 @@ namespace AppInstaller::CLI::Workflow return; } - // Get a mapping of source IDs to names so that we can show something nicer - std::map<std::string, std::string> sourceNames; - for (const auto& source : Repository::Source::GetCurrentSources()) - { - sourceNames[source.Identifier] = source.Name; - } - - Execution::TableOutput<4> table(context.Reporter, + Execution::TableOutput<6> table(context.Reporter, { + Resource::String::SearchName, Resource::String::SearchId, + Resource::String::SearchVersion, Resource::String::SearchSource, Resource::String::PinType, - Resource::String::SearchVersion, + Resource::String::PinVersion, }); + const auto& source = context.Get<Execution::Data::Source>(); for (const auto& pin : pins) { - table.OutputLine({ - pin.GetPackageId(), - sourceNames[pin.GetSourceId()], - std::string{ ToString(pin.GetType()) }, - pin.GetGatedVersion().ToString(), + const auto& pinKey = pin.GetKey(); + auto searchRequest = GetSearchRequestForPin(pin.GetKey()); + auto searchResult = source.Search(searchRequest); + for (const auto& match : searchResult.Matches) + { + Utility::LocIndString packageName; + Utility::LocIndString sourceName; + Utility::LocIndString version; + + if (pinKey.IsForInstalled()) + { + sourceName = Resource::LocString{ Resource::String::PinInstalledSource }; + } + else + { + // This ensures we get the info from the right source if it exists on multiple + auto availableVersion = match.Package->GetAvailableVersion({ pinKey.SourceId, "", "" }); + if (availableVersion) + { + packageName = availableVersion->GetProperty(PackageVersionProperty::Name); + sourceName = availableVersion->GetProperty(PackageVersionProperty::SourceName); + } + } + + auto installedVersion = match.Package->GetInstalledVersion(); + if (installedVersion) + { + packageName = installedVersion->GetProperty(PackageVersionProperty::Name); + version = installedVersion->GetProperty(PackageVersionProperty::Version); + } + + table.OutputLine({ + packageName, + pinKey.PackageId, + version, + sourceName, + std::string{ ToString(pin.GetType()) }, + pin.GetGatedVersion().ToString(), }); + } } table.Complete(); @@ -267,32 +316,4 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; } } - - void CrossReferencePinsWithSource(Execution::Context& context) - { - const auto& pins = context.Get<Execution::Data::Pins>(); - const auto& source = context.Get<Execution::Data::Source>(); - - std::vector<Pinning::Pin> matchingPins; - for (const auto& pin : pins) - - { - SearchRequest searchRequest; - searchRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, pin.GetPackageId()); - auto searchResult = source.Search(searchRequest); - - // Ensure the match comes from the right source - for (const auto& match : searchResult.Matches) - { - auto availableVersion = match.Package->GetAvailableVersion({ pin.GetSourceId(), "", "" }); - if (availableVersion) - { - matchingPins.push_back(pin); - } - } - } - - context.Add<Execution::Data::Pins>(std::move(matchingPins)); - } - } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h @@ -26,7 +26,8 @@ namespace AppInstaller::CLI::Workflow void GetAllPins(Execution::Context& context); // Searches for all the pins associated with a package. - // There may be several if a package is available from multiple sources. + // There may be several if a package is available from multiple sources + // or if the pin is for the installed package. // Required Args: None // Inputs: PinningIndex, Package // Outputs: Pins @@ -35,7 +36,7 @@ namespace AppInstaller::CLI::Workflow // Adds a pin for the current package. // A separate pin will be added for each source. // Required Args: None - // Inputs: PinningIndex, Package + // Inputs: PinningIndex, Package, InstalledVersion? // Outputs: None void AddPin(Execution::Context& context); @@ -46,6 +47,8 @@ namespace AppInstaller::CLI::Workflow void RemovePin(Execution::Context& context); // Report the pins in a table. + // This includes searching for the corresponding installed packages + // to be able to show more info, like the package name. // Required Args: None // Inputs: Pins // Outputs: None @@ -56,10 +59,4 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ResetAllPins(Execution::Context& context); - - // Updates the list of pins to include only those matching the current open source. - // Required Args: None - // Inputs: Pins, Source - // Outputs: None - void CrossReferencePinsWithSource(Execution::Context& context); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1583,6 +1583,13 @@ Please specify one of them using the --source option to proceed.</value> <data name="PinAddBlockingArgumentDescription" xml:space="preserve"> <value>Block from upgrading until the pin is removed, preventing override arguments</value> </data> + <data name="PinInstalledArgumentDescription" xml:space="preserve"> + <value>Pin a specific installed version</value> + </data> + <data name="PinInstalledSource" xml:space="preserve"> + <value>Installed</value> + <comment>Value used in a table to indicate that a package comes from the list of packages installed in the machine</comment> + </data> <data name="SettingsExportCommandLongDescription" xml:space="preserve"> <value>Export settings as JSON</value> </data> @@ -1825,6 +1832,10 @@ Please specify one of them using the --source option to proceed.</value> <data name="InstallWaitingOnAnother" xml:space="preserve"> <value>Waiting for another install/uninstall to complete...</value> </data> + <data name="PinVersion" xml:space="preserve"> + <value>Pinned version</value> + <comment>Table header for the version to which a package is pinned; meaning it should not update from that version.</comment> + </data> <data name="ConfigurationDescriptionWasTruncated" xml:space="preserve"> <value>&lt;See the log file for additional details&gt;</value> <comment>The brackets are intended to make the value stand out from other text which it will follow. Any locale appropriate mechanism that achieves this is acceptable.</comment> diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp @@ -122,6 +122,12 @@ struct TestPackageHelper return *this; } + TestPackageHelper& WithVersion(std::string_view version) + { + m_manifest.Version = version; + return *this; + } + TestPackageHelper& WithChannel(const std::string& channel) { m_manifest.Channel = channel; @@ -185,7 +191,7 @@ TestPackageHelper MakeAvailable(std::shared_ptr<ISource> source) return { /* isInstalled */ false, source}; } -void RequireIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatchField field, MatchType type, std::optional<std::string> value = {}) +bool SearchRequestIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatchField field, MatchType type, std::optional<std::string> value = {}) { bool found = false; @@ -198,7 +204,12 @@ void RequireIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatc } } - REQUIRE(found); + return found; +} + +void RequireSearchRequestIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatchField field, MatchType type, std::optional<std::string> value = {}) +{ + REQUIRE(SearchRequestIncludes(filters, field, type, value)); } TEST_CASE("CompositeSource_PackageFamilyName_NotAvailable", "[CompositeSource]") @@ -224,7 +235,7 @@ TEST_CASE("CompositeSource_PackageFamilyName_Available", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); @@ -260,7 +271,7 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPC(pc), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, pc); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, pc); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC(pc), Criteria()); @@ -280,7 +291,7 @@ TEST_CASE("CompositeSource_NameAndPublisher_Match", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(MakeInstalled(), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); @@ -347,7 +358,7 @@ TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -357,7 +368,7 @@ TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(availablePackage, Criteria()); @@ -378,7 +389,7 @@ TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") CompositeTestSetup setup; setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); @@ -388,7 +399,7 @@ TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") 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); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); @@ -410,7 +421,7 @@ TEST_CASE("CompositeSource_FoundByAvailableRootSearch_NotInstalled", "[Composite 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); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); @@ -436,7 +447,7 @@ TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(availablePackage, Criteria()); @@ -453,7 +464,7 @@ TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") // Now make the source root search find it with a better criteria setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -592,7 +603,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource setup.Available->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(firstName), Criteria()); @@ -601,7 +612,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource secondAvailable->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); @@ -630,7 +641,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou secondAvailable->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(secondName), Criteria()); @@ -657,7 +668,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -827,7 +838,7 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -839,12 +850,12 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") { if (request.Filters.empty()) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); } else { REQUIRE(request.Filters.size() == 1); - RequireIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); + RequireSearchRequestIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); } SearchResult result; @@ -875,7 +886,7 @@ TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -887,12 +898,12 @@ TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", { if (request.Filters.empty()) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); } else { REQUIRE(request.Filters.size() == 1); - RequireIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); + RequireSearchRequestIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); } SearchResult result; @@ -936,7 +947,7 @@ TEST_CASE("CompositeSource_TrackingFound_AvailableNot", "[CompositeSource]") setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -966,7 +977,7 @@ TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") setup.Installed->SearchFunction = [&](const SearchRequest& request) { - RequireIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); + RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; result.Matches.emplace_back(installedPackage, Criteria()); @@ -977,7 +988,7 @@ TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") setup.Available->SearchFunction = [&](const SearchRequest& request) { REQUIRE(request.Filters.size() == 1); - RequireIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); + RequireSearchRequestIncludes(request.Filters, PackageMatchField::Id, MatchType::CaseInsensitive, availableID); SearchResult result; result.Matches.emplace_back(availablePackage, Criteria()); @@ -1081,7 +1092,7 @@ void RequireExpectedResultsWithPin(std::shared_ptr<IPackage> package, const Expe } } -TEST_CASE("CompositeSource_PinnedAvailable", "[CompositeSource][PinFlow]") +TEST_CASE("CompositeSource_Pinning_AvailableVersionPinned", "[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 @@ -1176,7 +1187,7 @@ TEST_CASE("CompositeSource_PinnedAvailable", "[CompositeSource][PinFlow]") 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"}; + 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]; @@ -1196,7 +1207,7 @@ TEST_CASE("CompositeSource_PinnedAvailable", "[CompositeSource][PinFlow]") RequireExpectedResultsWithPin(package, expectedResult); } -TEST_CASE("CompositeSource_OneSourcePinned", "[CompositeSource][PinFlow]") +TEST_CASE("CompositeSource_Pinning_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. @@ -1254,7 +1265,7 @@ TEST_CASE("CompositeSource_OneSourcePinned", "[CompositeSource][PinFlow]") RequireExpectedResultsWithPin(package, expectedResult); } -TEST_CASE("CompositeSource_OneSourceGated", "[CompositeSource][PinFlow]") +TEST_CASE("CompositeSource_Pinning_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 @@ -1316,4 +1327,124 @@ TEST_CASE("CompositeSource_OneSourceGated", "[CompositeSource][PinFlow]") auto package = result.Matches[0].Package; REQUIRE(package); RequireExpectedResultsWithPin(package, expectedResult); -}- \ No newline at end of file +} + +TEST_CASE("CompositeSource_Pinning_MultipleInstalled", "[CompositeSource][PinFlow]") +{ + // Tests the case where multiple installed packages match to a single available package. + // If one of the two installed packages is pinned, when searching we should get + // two Composite packages, with only one of them pinned. + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + TestUserSettings userSettings; + userSettings.Set<Settings::Setting::EFPinning>(true); + + std::string packageId = "packageId"; + std::string productCode1 = "product-code1"; + std::string productCode2 = "product-code2"; + + // Installed packages differ in product code and version + auto installedPackage1 = MakeInstalled().WithId(productCode1).WithPC(productCode1).WithVersion("1.1"sv); + auto installedPackage2 = MakeInstalled().WithId(productCode2).WithPC(productCode2).WithVersion("1.2"sv); + + CompositeTestSetup setup; + setup.Installed->SearchFunction = [&](const SearchRequest& request) + { + bool isSearchById = SearchRequestIncludes(request.Inclusions, PackageMatchField::Id, MatchType::Exact, packageId); + + SearchResult result; + if (isSearchById || SearchRequestIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, productCode1)) + { + result.Matches.emplace_back(installedPackage1, Criteria()); + } + + if (isSearchById || SearchRequestIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, productCode2)) + { + result.Matches.emplace_back(installedPackage2, Criteria()); + } + + return result; + }; + + // Available package has the same ID, no product code, and different version from both the installed packages; + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(packageId).WithVersion("2.0"sv), Criteria()); + return result; + }; + + // We will pin the first package only + PinKey pinKey = PinKey::GetPinKeyForInstalled(productCode1); + auto pinningIndex = PinningIndex::OpenOrCreateDefault(); + REQUIRE(pinningIndex); + + // We will check the pinning status for both installed packages + ExpectedResultsForPinning expectedResult1; + ExpectedResultsForPinning expectedResult2; + + expectedResult1.ResultsForPinBehavior[PinBehavior::IgnorePins] + = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "2.0" }; + + // The second package is never pinned, so its result is always the same + expectedResult2.ResultsForPinBehavior[PinBehavior::IgnorePins] + = expectedResult2.ResultsForPinBehavior[PinBehavior::ConsiderPins] + = expectedResult2.ResultsForPinBehavior[PinBehavior::IncludePinned] + = { /* IsUpdateAvailable */ true, /* LatestAvailableVersion */ "2.0" }; + expectedResult2.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Unknown }, + }; + + SECTION("Unpinned") + { + // If there are no pins, the result should not change if we consider them + expectedResult1.ResultsForPinBehavior[PinBehavior::ConsiderPins] + = expectedResult1.ResultsForPinBehavior[PinBehavior::IncludePinned] + = expectedResult1.ResultsForPinBehavior[PinBehavior::IgnorePins]; + expectedResult1.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Unknown }, + }; + } + SECTION("Pinned") + { + pinningIndex->AddPin(Pin::CreatePinningPin(PinKey{ pinKey })); + + // Pinning pins are ignored with --include-pinned + expectedResult1.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult1.ResultsForPinBehavior[PinBehavior::IgnorePins]; + + expectedResult1.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ false, /* LatestAvailableVersion */ {} }; + expectedResult1.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Pinning }, + }; + } + SECTION("Blocked") + { + pinningIndex->AddPin(Pin::CreateBlockingPin(PinKey{ pinKey })); + expectedResult1.ResultsForPinBehavior[PinBehavior::ConsiderPins] = { /* IsUpdateAvailable */ false, /* LatestAvailableVersion */ {} }; + + // Blocking pins are not affected by --include-pinned + expectedResult1.ResultsForPinBehavior[PinBehavior::IncludePinned] = expectedResult1.ResultsForPinBehavior[PinBehavior::ConsiderPins]; + + expectedResult1.AvailableVersions = { + { "AvailableTestSource1", "2.0", "", Pinning::PinType::Blocking }, + }; + } + + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchField::Id, MatchType::Exact, packageId); + SearchResult result = setup.Composite.Search(searchRequest); + + REQUIRE(result.Matches.size() == 2); + + // Here we assume that the order we return the packages in the installed source + // search is preserved. We'll need to change it if that stops being the case. + auto package1 = result.Matches[0].Package; + REQUIRE(package1); + + auto package2 = result.Matches[1].Package; + REQUIRE(package2); + + RequireExpectedResultsWithPin(package1, expectedResult1); + RequireExpectedResultsWithPin(package2, expectedResult2); +} diff --git a/src/AppInstallerCLITests/MsixManifest.cpp b/src/AppInstallerCLITests/MsixManifest.cpp @@ -22,8 +22,8 @@ namespace constexpr std::string_view expectedFamilyName = "FakeInstallerForTesting_125rzkzqaqjwj"; PackageVersion expectedPackageVersion { 0xAAAABBBBCCCCDDDD }; constexpr std::string_view expectedWindowsDesktopName = "Windows.Desktop"; - OSVersion expectedWindowsDesktopMinVersion { "10.0.16299.0"}; - OSVersion expectedWindowsUniversalMinVersion { "10.0.0.0"}; + OSVersion expectedWindowsDesktopMinVersion { "10.0.16299.0" }; + OSVersion expectedWindowsUniversalMinVersion { "10.0.0.0" }; } TEST_CASE("MsixManifest_ValidateFieldsParsedFromManifestReader", "[MsixManifest]") diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp @@ -35,7 +35,7 @@ TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") std::ostringstream pinAddOutput; TestContext addContext{ pinAddOutput, std::cin }; OverrideForOpenPinningIndex(addContext, indexFile.GetPath()); - OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR:: TestInstaller_Exe })); + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); addContext.Args.AddArg(Execution::Args::Type::BlockingPin); @@ -49,9 +49,9 @@ TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") auto pins = index.GetAllPins(); REQUIRE(pins.size() == 1); REQUIRE(pins[0].GetType() == PinType::Blocking); - REQUIRE(pins[0].GetPackageId() == "AppInstallerCliTest.TestExeInstaller"); - REQUIRE(pins[0].GetSourceId() == "*TestSource"); REQUIRE(pins[0].GetGatedVersion().ToString() == ""); + REQUIRE(pins[0].GetKey().PackageId == "AppInstallerCliTest.TestExeInstaller"); + REQUIRE(pins[0].GetKey().SourceId == "*TestSource"); std::ostringstream pinListOutput; TestContext listContext{ pinListOutput, std::cin }; diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp @@ -77,8 +77,7 @@ TEST_CASE("PinningIndexAddEntryToTable", "[pinningIndex]") REQUIRE(pinFromIndex.value() == pin); REQUIRE(pinFromIndex->GetType() == pin.GetType()); - REQUIRE(pinFromIndex->GetPackageId() == pin.GetPackageId()); - REQUIRE(pinFromIndex->GetSourceId() == pin.GetSourceId()); + REQUIRE(pinFromIndex->GetKey() == pin.GetKey()); } { diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -280,7 +280,7 @@ TEST_CASE("Search_GoodResponse", "[RestSource][Interface_1_0]") "Publisher": "git", "Versions": [ { "PackageVersion": "1.0.0" }, - { "PackageVersion": "2.0.0"}] + { "PackageVersion": "2.0.0" }] }] })delimiter"); diff --git a/src/AppInstallerCLITests/RestInterface_1_1.cpp b/src/AppInstallerCLITests/RestInterface_1_1.cpp @@ -371,7 +371,7 @@ TEST_CASE("Search_BadRequest_UnsupportedPackageMatchFields", "[RestSource][Inter "Publisher": "git", "Versions": [ { "PackageVersion": "1.0.0" }, - { "PackageVersion": "2.0.0"}] + { "PackageVersion": "2.0.0" }] }] })delimiter"); @@ -394,7 +394,7 @@ TEST_CASE("Search_GoodRequest_OnlyMarketRequired", "[RestSource][Interface_1_1]" "Publisher": "git", "Versions": [ { "PackageVersion": "1.0.0" }, - { "PackageVersion": "2.0.0"}] + { "PackageVersion": "2.0.0" }] }] })delimiter"); diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -177,7 +177,7 @@ namespace TestCommon return InstalledVersion; } - std::vector<PackageVersionKey> TestPackage::GetAvailableVersionKeys() const + std::vector<PackageVersionKey> TestPackage::GetAvailableVersionKeys(PinBehavior) const { std::vector<PackageVersionKey> result; for (const auto& version : AvailableVersions) diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -63,7 +63,7 @@ namespace TestCommon AppInstaller::Utility::LocIndString GetProperty(AppInstaller::Repository::PackageProperty property) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetInstalledVersion() const override; - std::vector<AppInstaller::Repository::PackageVersionKey> GetAvailableVersionKeys() const override; + std::vector<AppInstaller::Repository::PackageVersionKey> GetAvailableVersionKeys(AppInstaller::Repository::PinBehavior) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetLatestAvailableVersion(AppInstaller::Repository::PinBehavior) const override; std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetAvailableVersion(const AppInstaller::Repository::PackageVersionKey& versionKey) const override; bool IsUpdateAvailable(AppInstaller::Repository::PinBehavior) const override; diff --git a/src/AppInstallerCommonCore/Pin.cpp b/src/AppInstallerCommonCore/Pin.cpp @@ -9,6 +9,38 @@ namespace AppInstaller::Pinning { using namespace std::string_view_literals; + namespace + { + // Source ID to use for the installed source; it does not match any installed source. + // This does match with the actual ID of the source, but it doesn't really matter + // as it is handled specially when we see it. + constexpr std::string_view s_installedSourceId = "*PredefinedInstalledSource"sv; + } + + PinType ConvertToPinTypeEnum(std::string_view in) + { + if (Utility::CaseInsensitiveEquals(in, "Blocking"sv)) + { + return PinType::Blocking; + } + else if (Utility::CaseInsensitiveEquals(in, "Pinning"sv)) + { + return PinType::Pinning; + } + else if (Utility::CaseInsensitiveEquals(in, "Gating"sv)) + { + return PinType::Gating; + } + else if (Utility::CaseInsensitiveEquals(in, "PinnedByManifest"sv)) + { + return PinType::PinnedByManifest; + } + else + { + return PinType::Unknown; + } + } + std::string_view ToString(PinType type) { switch (type) @@ -27,28 +59,39 @@ namespace AppInstaller::Pinning } } - PinType ConvertToPinTypeEnum(std::string_view in) + bool IsStricter(PinType first, PinType second) { - if (Utility::CaseInsensitiveEquals(in, "Blocking"sv)) - { - return PinType::Blocking; + return first > second; } - else if (Utility::CaseInsensitiveEquals(in, "Pinning"sv)) + + std::string PinKey::ToString() const { - return PinType::Pinning; + std::stringstream ss; + ss << "Package=[" << PackageId << "] Source=[" << SourceId << "]"; + return ss.str(); } - else if (Utility::CaseInsensitiveEquals(in, "Gating"sv)) + + PinKey PinKey::GetPinKeyForInstalled(std::string_view systemReferenceString) { - return PinType::Gating; + return { systemReferenceString, s_installedSourceId }; } - else if (Utility::CaseInsensitiveEquals(in, "PinnedByManifest"sv)) + + bool PinKey::IsForInstalled() const { - return PinType::PinnedByManifest; + return SourceId == s_installedSourceId; } - else + + std::string Pin::ToString() const { - return PinType::Unknown; + std::stringstream ss; + ss << m_key.ToString() << " Type=[" << Pinning::ToString(m_type) << ']'; + + if (m_type == PinType::Gating) + { + ss << " GatedVersion=[" << m_gatedVersion.ToString() << ']'; } + + return ss.str(); } Pin Pin::CreateBlockingPin(PinKey&& pinKey) diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -6,48 +6,73 @@ namespace AppInstaller::Pinning { - enum class PinType + // The pin types are ordered by how "strict" they are. + // Meaning, the one that is more restrictive goes later. + // This is used to decide which pin to report if there are multiple pins. + enum class PinType : int64_t { // Unknown pin type or not pinned Unknown, // Pinned by the manifest using the RequiresExplicitUpgrade field. // Behaves the same as Pinning pins PinnedByManifest, - // The package is blocked from 'upgrade --all' and 'upgrade <package>'. - // User has to unblock to allow update. - Blocking, // The package is excluded from 'upgrade --all', unless '--include-pinned' is added. // 'upgrade <package>' is not blocked. Pinning, // The package is pinned to a specific version range. Gating, + // The package is blocked from 'upgrade --all' and 'upgrade <package>'. + // User has to unblock to allow update. + Blocking, }; std::string_view ToString(PinType type); PinType ConvertToPinTypeEnum(std::string_view in); - // The set of values needed to uniquely identify a Pin + // Determines which of two pin types is more strict. + bool IsStricter(PinType first, PinType second); + + // The set of values needed to uniquely identify a Pin. + // A Pin can apply to an installed package or to an available package. + // Pins on available packages can persist when an app is updated outside of winget, + // but it's hard to have them work when there are multiple installed packages for the same available package. + // Pins on installed packages work fine when there are multiple installed packages for the same available, + // but they break when the package is updated outside of winget. struct PinKey { PinKey() {} PinKey(const Manifest::Manifest::string_t& packageId, std::string_view sourceId) : PackageId(packageId), SourceId(sourceId) {} + // Gets a pin key that refers to an installed package by its ProductCode or PackageFamilyName. + // The sourceId used is a special string to distinguish from available packages. + static PinKey GetPinKeyForInstalled(std::string_view systemReferenceString); + + bool IsForInstalled() const; + bool operator==(const PinKey& other) const { - return PackageId == other.PackageId && SourceId == other.SourceId; + return PackageId == other.PackageId + && SourceId == other.SourceId; } + bool operator!=(const PinKey& other) const { return !(*this == other); } + bool operator<(const PinKey& other) const { - return PackageId < other.PackageId || (PackageId == other.PackageId && SourceId < other.SourceId); + // std::tie implements tuple comparison, wherein it checks the first item in the tuple, + // iff the first elements are equal, then the second element is used for comparison, and so on + return std::tie(PackageId, SourceId) < std::tie(other.PackageId, other.SourceId); } - Manifest::Manifest::string_t PackageId; - std::string SourceId; + // Used for logging + std::string ToString() const; + + const std::string PackageId; + const std::string SourceId; }; struct Pin @@ -56,13 +81,22 @@ namespace AppInstaller::Pinning static Pin CreatePinningPin(PinKey&& pinKey); static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion); + static Pin CreateBlockingPin(const PinKey& pinKey) { return CreateBlockingPin(PinKey{ pinKey }); } + static Pin CreatePinningPin(const PinKey& pinKey) { return CreatePinningPin(PinKey{ pinKey }); } + static Pin CreateGatingPin(const PinKey& pinKey, const Utility::GatedVersion& gatedVersion) { return CreateGatingPin(PinKey{ pinKey }, Utility::GatedVersion{ gatedVersion }); } + 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; + bool operator<(const Pin& other) const + { + return std::make_pair(m_type, m_key) < std::make_pair(other.m_type, other.m_key); + } + + // Used for logging + std::string ToString() const; private: Pin(PinType type, PinKey&& pinKey, Utility::GatedVersion&& gatedVersion = {}) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -22,7 +22,7 @@ namespace AppInstaller::Repository }; } - Pinning::PinKey GetPinKey(IPackage* availablePackage) + Pinning::PinKey GetPinKeyForAvailable(IPackage* availablePackage) { return { availablePackage->GetProperty(PackageProperty::Id).get(), @@ -30,36 +30,82 @@ namespace AppInstaller::Repository }; } - std::optional<PackageVersionKey> GetLatestAvailableVersionKeySatisfyingPin(const std::vector<PackageVersionKey>& availableVersionKeys, PinBehavior pinBehavior) + // Gets the pinned state for an available PackageVersionKey that may have a pin, + // and optionally an additional pin that could come from the installed version or + // be a pin we have not considered yet for the version key. + // If there are a pin both in the version key and passed as an argument, + // we return the one that is the most strict. + // Note that for a package with both available and installed pins, we will call this + // twice: once with the available pin to set the pinned state in the version key, + // and once with the installed pin to set the final pinned state. + Pinning::PinType GetPinnedStateForVersion( + const PackageVersionKey& availableVersionKey, + const std::optional<Pinning::Pin>& pin, + PinBehavior pinBehavior) + { + if (pinBehavior == PinBehavior::IgnorePins) + { + // No need to check anything + return Pinning::PinType::Unknown; + } + + // For the pin in the version version, we can ignore it depending on the behavior and type. + // If it is gating, we don't need to check the version as that was already done when the + // PinnedState info was added (and we don't have the gated version here). + Pinning::PinType pinnedStateFromVersionKey = Pinning::PinType::Unknown; + if (availableVersionKey.PinnedState == Pinning::PinType::Blocking + || (availableVersionKey.PinnedState == Pinning::PinType::Pinning && pinBehavior != PinBehavior::IncludePinned) + || availableVersionKey.PinnedState == Pinning::PinType::Gating) + { + pinnedStateFromVersionKey = availableVersionKey.PinnedState; + } + + // For the additional pin, we can ignore it depending on the behavior and type. + // If it is gating, we need to check the version. + Pinning::PinType pinnedStateFromAdditionalPin = Pinning::PinType::Unknown; + if (pin) + { + if (pin->GetType() == Pinning::PinType::Blocking + || (pin->GetType() == Pinning::PinType::Pinning && pinBehavior != PinBehavior::IncludePinned) + || (pin->GetType() == Pinning::PinType::Gating && !pin->GetGatedVersion().IsValidVersion(availableVersionKey.Version))) + { + pinnedStateFromAdditionalPin = pin->GetType(); + } + } + + return Pinning::IsStricter(pinnedStateFromVersionKey, pinnedStateFromAdditionalPin) ? pinnedStateFromVersionKey : pinnedStateFromAdditionalPin; + } + + // Gets the latest available version that satisfies both the available pin (already tagged on the keys) + // and the installed pin (if any). + // Version keys must be sorted with the latest first. + std::optional<PackageVersionKey> GetLatestAvailableVersionKeySatisfyingPin( + const std::vector<PackageVersionKey>& availableVersionKeys, + const std::optional<Pinning::Pin>& installedPin, + PinBehavior pinBehavior) { if (availableVersionKeys.empty()) { return {}; } - std::optional<PackageVersionKey> pvk; if (pinBehavior == PinBehavior::IgnorePins) { - pvk = availableVersionKeys.front(); + return 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)) + if (GetPinnedStateForVersion(availableVersion, installedPin, pinBehavior) == Pinning::PinType::Unknown) { - continue; + return availableVersion; } - - pvk = availableVersion; - break; } } - return pvk; + return {}; } // Returns true for fields that provide a strong match; one that is not based on a heuristic. @@ -369,18 +415,21 @@ namespace AppInstaller::Repository std::shared_ptr<IPackageVersion> m_trackingPackageVersion; }; - // Wrapper around an available package to add pinning functionality for composite packages. + // Wrapper around a 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 + // A pinnable package can either be an installed package or a single available package; + // we deal with composite packages on CompositePackage. + struct PinnablePackage : public IPackage { - CompositeAvailablePackage() {} - CompositeAvailablePackage(std::shared_ptr<IPackage> availablePackage, std::optional<Pinning::Pin> pin = {}) - : m_availablePackage(availablePackage), m_pin(pin) + PinnablePackage() {} + PinnablePackage(std::shared_ptr<IPackage> package, std::optional<Pinning::Pin> pin = {}) + : m_package(package), m_pin(pin) { - auto latestAvailable = m_availablePackage->GetLatestAvailableVersion(PinBehavior::IgnorePins); - if (latestAvailable) + // Get the source ID for available packages + auto availableVersion = m_package->GetLatestAvailableVersion(PinBehavior::IgnorePins); + if (availableVersion) { - m_sourceId = latestAvailable->GetSource().GetIdentifier(); + m_sourceId = availableVersion->GetSource().GetIdentifier(); } } @@ -389,9 +438,9 @@ namespace AppInstaller::Repository return m_sourceId; } - const std::shared_ptr<IPackage>& GetAvailablePackage() const + const std::shared_ptr<IPackage>& GetPackage() const { - return m_availablePackage; + return m_package; } const std::optional<Pinning::Pin>& GetPin() const @@ -401,32 +450,28 @@ namespace AppInstaller::Repository void SetPin(Pinning::Pin&& pin) { - m_pin = std::move(pin); + m_pin.emplace(std::move(pin)); } Utility::LocIndString GetProperty(PackageProperty property) const override { - return m_availablePackage->GetProperty(property); + return m_package->GetProperty(property); } std::shared_ptr<IPackageVersion> GetInstalledVersion() const override { - return {}; + return m_package->GetInstalledVersion(); } - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior pinBehavior) const override { - auto result = m_availablePackage->GetAvailableVersionKeys(); + auto result = m_package->GetAvailableVersionKeys(); if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning) && m_pin.has_value()) { + // Add pin information to all version keys 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(); - } + pvk.PinnedState = GetPinnedStateForVersion(pvk, m_pin, pinBehavior); } } @@ -440,8 +485,8 @@ namespace AppInstaller::Repository std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const override { - auto availableVersionKeys = GetAvailableVersionKeys(); - auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, pinBehavior); + auto availableVersionKeys = GetAvailableVersionKeys(pinBehavior); + auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, /* installedPin */ {}, pinBehavior); if (!latestVersionKey) { return {}; @@ -456,14 +501,10 @@ namespace AppInstaller::Repository 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(); - } + pinType = GetPinnedStateForVersion(versionKey, m_pin.value(), PinBehavior::ConsiderPins); } - return { m_availablePackage->GetAvailableVersion(versionKey), pinType }; + return { m_package->GetAvailableVersion(versionKey), pinType }; } bool IsUpdateAvailable(PinBehavior) const override @@ -473,14 +514,14 @@ namespace AppInstaller::Repository bool IsSame(const IPackage* other) const override { - const CompositeAvailablePackage* otherAvailable = dynamic_cast<const CompositeAvailablePackage*>(other); + const PinnablePackage* otherAvailable = dynamic_cast<const PinnablePackage*>(other); if (otherAvailable) { return m_sourceId == otherAvailable->m_sourceId && m_pin == otherAvailable->m_pin && - m_availablePackage->IsSame(otherAvailable->m_availablePackage.get()); + m_package->IsSame(otherAvailable->m_package.get()); } return false; @@ -488,19 +529,19 @@ namespace AppInstaller::Repository private: std::string m_sourceId; - std::shared_ptr<IPackage> m_availablePackage; + std::shared_ptr<IPackage> m_package; std::optional<Pinning::Pin> m_pin; }; // A composite package for the CompositeSource. struct CompositePackage : public IPackage { - CompositePackage(std::shared_ptr<IPackage> installedPackage, std::shared_ptr<IPackage> availablePackage = {}) : - m_installedPackage(std::move(installedPackage)) + CompositePackage(std::shared_ptr<IPackage> installedPackage, std::shared_ptr<IPackage> availablePackage = {}) { // Grab the installed version's channel to allow for filtering in calls to get available info. - if (m_installedPackage) + if (installedPackage) { + m_installedPackage.emplace(installedPackage); auto installedVersion = m_installedPackage->GetInstalledVersion(); if (installedVersion) { @@ -556,13 +597,25 @@ namespace AppInstaller::Repository return {}; } - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior pinBehavior) const override { std::vector<PackageVersionKey> result; + auto installedPin = GetInstalledPin(); for (const auto& availablePackage : m_availablePackages) { - auto versionKeys = availablePackage.GetAvailableVersionKeys(); + auto versionKeys = availablePackage.GetAvailableVersionKeys(pinBehavior); + + // The version keys we have already have pin information from the available package. + // Here we also add information from the installed package. + if (installedPin) + { + for (auto& versionKey : versionKeys) + { + versionKey.PinnedState = GetPinnedStateForVersion(versionKey, installedPin, pinBehavior); + } + } + std::copy(versionKeys.begin(), versionKeys.end(), std::back_inserter(result)); } @@ -580,8 +633,8 @@ namespace AppInstaller::Repository std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const override { - auto availableVersionKeys = GetAvailableVersionKeys(); - auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, pinBehavior); + auto availableVersionKeys = GetAvailableVersionKeys(pinBehavior); + auto latestVersionKey = GetLatestAvailableVersionKeySatisfyingPin(availableVersionKeys, GetInstalledPin(), pinBehavior); if (!latestVersionKey) { return {}; @@ -607,6 +660,7 @@ namespace AppInstaller::Repository auto result = availablePackage.GetAvailableVersionAndPin(versionKey); if (result.first) { + result.second = GetPinnedStateForVersion(versionKey, GetInstalledPin(), PinBehavior::ConsiderPins); return result; } } @@ -634,16 +688,20 @@ namespace AppInstaller::Repository if (!otherComposite || static_cast<bool>(m_installedPackage) != static_cast<bool>(otherComposite->m_installedPackage) || - (m_installedPackage && !m_installedPackage->IsSame(otherComposite->m_installedPackage.get())) || m_availablePackages.size() != otherComposite->m_availablePackages.size()) { return false; } + if (m_installedPackage && !m_installedPackage->GetPackage()->IsSame(otherComposite->m_installedPackage->GetPackage().get())) + { + 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())) + !m_availablePackages[i].GetPackage()->IsSame(otherComposite->m_availablePackages[i].GetPackage().get())) { return false; } @@ -658,7 +716,7 @@ namespace AppInstaller::Repository { for (const auto& availablePackage : m_availablePackages) { - if (other->IsSame(availablePackage.GetAvailablePackage().get())) + if (other->IsSame(availablePackage.GetPackage().get())) { return true; } @@ -668,9 +726,16 @@ namespace AppInstaller::Repository return false; } - const std::shared_ptr<IPackage>& GetInstalledPackage() const + std::shared_ptr<IPackage> GetInstalledPackage() const { - return m_installedPackage; + if (m_installedPackage) + { + return m_installedPackage->GetPackage(); + } + else + { + return {}; + } } const std::shared_ptr<IPackage>& GetTrackingPackage() const @@ -701,24 +766,29 @@ namespace AppInstaller::Repository } // Gets the information about the pins that exist for this package - void GetExistingPins(PinningIndex& pinningIndex, bool cleanUpStalePins) + void GetExistingPins(PinningIndex& pinningIndex) { - // 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) + Pinning::PinKey pinKey = GetPinKeyForAvailable(availablePackage.GetPackage().get()); + + auto pin = pinningIndex.GetPin(pinKey); + if (pin.has_value()) { - auto pin = pinningIndex.GetPin(pinKey); - if (pin.has_value()) - { - availablePackage.SetPin(std::move(pin.value())); - } + availablePackage.SetPin(std::move(pin.value())); } - else if (pinningIndex.GetPin(pinKey) && cleanUpStalePins) + } + + if (m_installedPackage) + { + Pinning::PinKey pinKey = Pinning::PinKey::GetPinKeyForInstalled( + m_installedPackage->GetProperty(PackageProperty::Id).get() + ); + + auto pin = pinningIndex.GetPin(pinKey); + if (pin.has_value()) { - pinningIndex.RemovePin(pinKey); + m_installedPackage->SetPin(std::move(pin.value())); } } } @@ -741,14 +811,21 @@ namespace AppInstaller::Repository } } - std::shared_ptr<IPackage> m_installedPackage; + std::optional<Pinning::Pin> GetInstalledPin() const + { + return (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Pinning) && m_installedPackage) + ? m_installedPackage->GetPin() + : std::nullopt; + } + + std::optional<PinnablePackage> m_installedPackage; Utility::LocIndString m_installedChannel; Source m_trackingSource; std::shared_ptr<IPackage> m_trackingPackage; std::shared_ptr<IPackageVersion> m_trackingPackageVersion; std::string m_overrideInstalledVersion; std::shared_ptr<IPackage> m_defaultAvailablePackage; - std::vector<CompositeAvailablePackage> m_availablePackages; + std::vector<PinnablePackage> m_availablePackages; }; // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. @@ -1093,8 +1170,6 @@ namespace AppInstaller::Repository } // 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()) @@ -1105,7 +1180,7 @@ namespace AppInstaller::Repository { for (auto& match : result.Matches) { - match.Package->GetExistingPins(*pinningIndex, /* cleanUpStalePins */ true); + match.Package->GetExistingPins(*pinningIndex); } } } diff --git a/src/AppInstallerRepositoryCore/InstalledFilesCorrelation.cpp b/src/AppInstallerRepositoryCore/InstalledFilesCorrelation.cpp @@ -16,7 +16,7 @@ namespace AppInstaller::Repository::Correlation constexpr std::string_view s_ShellLinkFileExtension = ".lnk"sv; const std::vector<std::pair<std::filesystem::path, std::string>> s_CandidateInstallLocationRoots = { - { Filesystem::GetKnownFolderPath(FOLDERID_LocalAppData), "%LOCALAPPDATA%"}, + { Filesystem::GetKnownFolderPath(FOLDERID_LocalAppData), "%LOCALAPPDATA%" }, { Filesystem::GetKnownFolderPath(FOLDERID_ProgramFiles), "%PROGRAMFILES%" }, { Filesystem::GetKnownFolderPath(FOLDERID_ProgramFilesX86), "%PROGRAMFILES(X86)%" }, }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -9,7 +9,7 @@ namespace AppInstaller::Repository::Microsoft { PinningIndex PinningIndex::CreateNew(const std::string& filePath, Schema::Version version) { - AICLI_LOG(Repo, Info, << "Creating new Pinning Index [" << version << "] at '" << filePath << "'"); + AICLI_LOG(Repo, Info, << "Creating new Pinning Index with version [" << version << "] at '" << filePath << "'"); PinningIndex result{ filePath, version }; SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "pinningindex_createnew"); @@ -74,7 +74,7 @@ namespace AppInstaller::Repository::Microsoft PinningIndex::IdType PinningIndex::AddPin(const Pinning::Pin& pin) { std::lock_guard<std::mutex> lockInterface{ *m_interfaceLock }; - AICLI_LOG(Repo, Verbose, << "Adding Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType())); + AICLI_LOG(Repo, Verbose, << "Adding Pin " << pin.ToString()); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_addpin"); @@ -90,7 +90,7 @@ namespace AppInstaller::Repository::Microsoft bool PinningIndex::UpdatePin(const Pinning::Pin& pin) { std::lock_guard<std::mutex> lockInterface{ *m_interfaceLock }; - AICLI_LOG(Repo, Verbose, << "Updating Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType())); + AICLI_LOG(Repo, Verbose, << "Updating Pin " << pin.ToString()); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_updatepin"); @@ -121,7 +121,7 @@ namespace AppInstaller::Repository::Microsoft void PinningIndex::RemovePin(const Pinning::PinKey& pinKey) { std::lock_guard<std::mutex> lockInterface{ *m_interfaceLock }; - AICLI_LOG(Repo, Verbose, << "Removing Pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + AICLI_LOG(Repo, Verbose, << "Removing Pin " << pinKey.ToString()); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningIndex_removePin"); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -10,7 +10,7 @@ namespace AppInstaller::Repository::Microsoft { PortableIndex PortableIndex::CreateNew(const std::string& filePath, Schema::Version version) { - AICLI_LOG(Repo, Info, << "Creating new Portable Index [" << version << "] at '" << filePath << "'"); + AICLI_LOG(Repo, Info, << "Creating new Portable Index with version [" << version << "] at '" << filePath << "'"); PortableIndex result{ filePath, version }; SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "portableindex_createnew"); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -18,7 +18,7 @@ namespace AppInstaller::Repository::Microsoft { SQLiteIndex SQLiteIndex::CreateNew(const std::string& filePath, Schema::Version version, CreateOptions options) { - AICLI_LOG(Repo, Info, << "Creating new SQLite Index [" << version << "] at '" << filePath << "'"); + AICLI_LOG(Repo, Info, << "Creating new SQLite Index with version [" << version << "] at '" << filePath << "'"); SQLiteIndex result{ filePath, version }; SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "sqliteindex_createnew"); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -263,7 +263,7 @@ namespace AppInstaller::Repository::Microsoft return {}; } - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior) const override { std::shared_ptr<SQLiteIndexSource> source = GetReferenceSource(); std::vector<Utility::VersionAndChannel> versions = source->GetIndex().GetVersionKeysById(m_idId); @@ -335,7 +335,7 @@ namespace AppInstaller::Repository::Microsoft return GetLatestVersionInternal(); } - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior) const override { return {}; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp @@ -9,7 +9,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 { namespace { - std::optional<Pinning::Pin> GetPinFromRow(std::string_view packageId, std::string_view sourceId, Pinning::PinType type, std::string_view version) + std::optional<Pinning::Pin> GetPinFromRow( + std::string_view packageId, + std::string_view sourceId, + Pinning::PinType type, + std::string_view version) { switch (type) @@ -51,14 +55,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 createTableBuilder.Column(ColumnBuilder(s_PinTable_PackageId_Column, Type::Text).NotNull()); createTableBuilder.Column(ColumnBuilder(s_PinTable_SourceId_Column, Type::Text).NotNull()); createTableBuilder.Column(ColumnBuilder(s_PinTable_Type_Column, Type::Int64).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_Version_Column, Type::Text)); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Version_Column, Type::Text).NotNull()); createTableBuilder.EndColumns(); createTableBuilder.Execute(connection); // Create an index over the pairs package,source StatementBuilder createIndexBuilder; - createIndexBuilder.CreateUniqueIndex(s_PinTable_Index).On(s_PinTable_Table_Name).Columns({ s_PinTable_PackageId_Column, s_PinTable_SourceId_Column }); + createIndexBuilder.CreateUniqueIndex(s_PinTable_Index).On(s_PinTable_Table_Name) + .Columns({ s_PinTable_PackageId_Column, s_PinTable_SourceId_Column }); createIndexBuilder.Execute(connection); savepoint.Commit(); @@ -86,6 +91,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); builder.InsertInto(s_PinTable_Table_Name) .Columns({ s_PinTable_PackageId_Column, @@ -93,8 +99,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 s_PinTable_Type_Column, s_PinTable_Version_Column }) .Values( - (std::string_view)pin.GetPackageId(), - pin.GetSourceId(), + (std::string_view)pinKey.PackageId, + pinKey.SourceId, pin.GetType(), pin.GetGatedVersion().ToString()); @@ -105,9 +111,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 bool PinTable::UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) { SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); builder.Update(s_PinTable_Table_Name).Set() - .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pin.GetPackageId()) - .Column(s_PinTable_SourceId_Column).Equals(pin.GetSourceId()) + .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) .Column(s_PinTable_Type_Column).Equals(pin.GetType()) .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) .Where(SQLite::RowIDName).Equals(pinId); @@ -140,8 +147,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return {}; } - auto [packageId, sourceId, type, gatedVersion] = select.GetRow<std::string, std::string, Pinning::PinType, std::string>(); - return GetPinFromRow(packageId, sourceId, type, gatedVersion); + auto [packageId, sourceId, pinType, gatedVersion] = + select.GetRow<std::string, std::string, Pinning::PinType, std::string>(); + return GetPinFromRow(packageId, sourceId, pinType, gatedVersion); } std::vector<Pinning::Pin> PinTable::GetAllPins(SQLite::Connection& connection) @@ -159,8 +167,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 std::vector<Pinning::Pin> pins; while (select.Step()) { - auto [packageId, sourceId, type, gatedVersion] = select.GetRow<std::string, std::string, Pinning::PinType, std::string>(); - auto pin = GetPinFromRow(packageId, sourceId, type, gatedVersion); + auto [packageId, sourceId, pinType, gatedVersion] = + select.GetRow<std::string, std::string, Pinning::PinType, std::string>(); + auto pin = GetPinFromRow(packageId, sourceId, pinType, gatedVersion); if (pin) { pins.push_back(std::move(pin.value())); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp @@ -14,7 +14,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 if (!result) { - AICLI_LOG(Repo, Verbose, << "Did not find a pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + AICLI_LOG(Repo, Verbose, << "Did not find pin " << pinKey.ToString()); } return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp @@ -26,6 +26,17 @@ namespace AppInstaller::Repository::Microsoft::Schema std::ostream& operator<<(std::ostream& out, const Version& version) { - return (out << version.MajorVersion << '.' << version.MinorVersion); + if (version.IsLatest()) + { + return out << "Latest"; + } + else if (version.IsLatestForMajor(version.MajorVersion)) + { + return out << version.MajorVersion << ".Latest"; + } + else + { + return out << version.MajorVersion << '.' << version.MinorVersion; + } } } diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -349,7 +349,7 @@ namespace AppInstaller::Repository // The versions will be returned in sorted, descending order. // Ex. { 4, 3, 2, 1 } // The list may contain versions from multiple sources. - virtual std::vector<PackageVersionKey> GetAvailableVersionKeys() const = 0; + virtual std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior pinBehavior = PinBehavior::ConsiderPins) const = 0; // Gets a specific version of this package. virtual std::shared_ptr<IPackageVersion> GetLatestAvailableVersion(PinBehavior pinBehavior) const = 0; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -57,7 +57,7 @@ namespace AppInstaller::Repository::Rest return {}; } - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys(PinBehavior) const override { std::shared_ptr<const RestSource> source = GetReferenceSource(); std::scoped_lock versionsLock{ m_packageVersionsLock };