commit 959aacb3cb6573931d6aea30f591dfeb8bfd872c parent c3a05309568f1cb8c81017ab1aacd093c409a555 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 20 Oct 2020 02:11:31 -0700 List command initial implementation behind feature toggle (#598) This phase introduces the `list` command and much of the associative plumbing. It currently only handles enumerating MSIX packages from the system, but it should be able to support additional enumeration simply by adding more to the index. The goal of the `list` command is to enumerate packages installed on the machine, as well as to indicate which packages have an update available from a configured source. This is achieved by creating predefined sources for installed packages, and a `CompositeSource` class to do the work of correlating the installed packages with those from external sources. Diffstat:
62 files changed, 2746 insertions(+), 417 deletions(-)
diff --git a/doc/Settings.md b/doc/Settings.md @@ -67,3 +67,23 @@ Microsoft Store App support in WinGet is currently implemented as an experimenta "experimentalMSStore": true }, ``` + +### list + +While work is in progress on list, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "list": true + }, +``` + +### upgrade + +While work is in progress on upgrade, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "upgrade": true + }, +``` diff --git a/doc/settings.schema.json b/doc/settings.schema.json @@ -47,6 +47,16 @@ "description": "Experimental support for Microsoft Store source", "type": "boolean", "default": "false" + }, + "list": { + "description": "Enable the list command while it is in development", + "type": "boolean", + "default": "false" + }, + "upgrade": { + "description": "Enable the upgrade command while it is in development", + "type": "boolean", + "default": "false" } } } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project", "Project", "{8D53 ..\azure-pipelines.yml = ..\azure-pipelines.yml ..\cgmanifest.json = ..\cgmanifest.json ..\README.md = ..\README.md + ..\doc\Settings.md = ..\doc\Settings.md + ..\doc\settings.schema.json = ..\doc\settings.schema.json EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "catch2", "catch2\catch2.vcxitems", "{5295E21E-9868-4DE2-A177-FBB97B36579B}" diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -179,6 +179,7 @@ <ClInclude Include="Commands\ExperimentalCommand.h" /> <ClInclude Include="Commands\FeaturesCommand.h" /> <ClInclude Include="Commands\HashCommand.h" /> + <ClInclude Include="Commands\ListCommand.h" /> <ClInclude Include="Commands\SearchCommand.h" /> <ClInclude Include="Commands\ShowCommand.h" /> <ClInclude Include="Commands\InstallCommand.h" /> @@ -217,6 +218,7 @@ <ClCompile Include="Commands\ExperimentalCommand.cpp" /> <ClCompile Include="Commands\FeaturesCommand.cpp" /> <ClCompile Include="Commands\HashCommand.cpp" /> + <ClCompile Include="Commands\ListCommand.cpp" /> <ClCompile Include="Commands\SearchCommand.cpp" /> <ClCompile Include="Commands\ShowCommand.cpp" /> <ClCompile Include="Commands\InstallCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -132,6 +132,9 @@ <ClInclude Include="Commands\UpgradeCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Commands\ListCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -230,6 +233,9 @@ <ClCompile Include="Commands\UpgradeCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClCompile Include="Commands\ListCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Commands/ListCommand.cpp b/src/AppInstallerCLICore/Commands/ListCommand.cpp @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ListCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + std::vector<Argument> ListCommand::GetArguments() const + { + return { + Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Id), + Argument::ForType(Execution::Args::Type::Name), + Argument::ForType(Execution::Args::Type::Moniker), + Argument::ForType(Execution::Args::Type::Source), + Argument::ForType(Execution::Args::Type::Tag), + Argument::ForType(Execution::Args::Type::Command), + Argument::ForType(Execution::Args::Type::Count), + Argument::ForType(Execution::Args::Type::Exact), + }; + } + + Resource::LocString ListCommand::ShortDescription() const + { + return { Resource::String::ListCommandShortDescription }; + } + + Resource::LocString ListCommand::LongDescription() const + { + return { Resource::String::ListCommandLongDescription }; + } + + void ListCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + { + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + switch (valueType) + { + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Source: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); + break; + } + } + + std::string ListCommand::HelpLink() const + { + return "https://aka.ms/winget-command-list"; + } + + void ListCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::SearchSourceForMany << + Workflow::EnsureMatchesFromSearchResult << + Workflow::ReportListResult(); + } +} diff --git a/src/AppInstallerCLICore/Commands/ListCommand.h b/src/AppInstallerCLICore/Commands/ListCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + // Command to get the set of installed packages on the system. + struct ListCommand final : public Command + { + ListCommand(std::string_view parent) : Command("list", parent, Settings::ExperimentalFeature::Feature::ExperimentalList) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -7,6 +7,8 @@ #include "ShowCommand.h" #include "SourceCommand.h" #include "SearchCommand.h" +#include "ListCommand.h" +#include "UpgradeCommand.h" #include "HashCommand.h" #include "ValidateCommand.h" #include "SettingsCommand.h" @@ -28,6 +30,8 @@ namespace AppInstaller::CLI std::make_unique<ShowCommand>(FullName()), std::make_unique<SourceCommand>(FullName()), std::make_unique<SearchCommand>(FullName()), + std::make_unique<ListCommand>(FullName()), + std::make_unique<UpgradeCommand>(FullName()), std::make_unique<HashCommand>(FullName()), std::make_unique<ValidateCommand>(FullName()), std::make_unique<SettingsCommand>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -47,9 +47,45 @@ namespace AppInstaller::CLI return { Resource::String::UpgradeCommandLongDescription }; } - void UpgradeCommand::Complete(Execution::Context&, Execution::Args::Type) const + void UpgradeCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const { - // TODO: Should be done similar to list completion + if (valueType == Execution::Args::Type::Manifest || + valueType == Execution::Args::Type::Log || + valueType == Execution::Args::Type::Override || + valueType == Execution::Args::Type::InstallLocation) + { + // Intentionally output nothing to allow pass through to filesystem. + return; + } + + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + switch (valueType) + { + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Version: + case Execution::Args::Type::Channel: + case Execution::Args::Type::Source: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); + break; + case Execution::Args::Type::Language: + // May well move to CompleteWithSingleSemanticsForValue, + // but for now output nothing. + context << + Workflow::CompleteWithEmptySet; + break; + } } std::string UpgradeCommand::HelpLink() const @@ -81,12 +117,15 @@ namespace AppInstaller::CLI context << OpenSource << - GetCompositeSourceFromInstalledAndAvailable; + OpenCompositeSource(Repository::PredefinedSource::Installed); if (context.Args.Empty()) { // Upgrade with no args list packages with updates available - // TODO: go to list filtered to packages with update available + context << + Workflow::SearchSourceForMany << + Workflow::EnsureMatchesFromSearchResult << + Workflow::ReportListResult(true); } else if (context.Args.Contains(Execution::Args::Type::All)) { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -31,6 +31,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFoundError); WINGET_DEFINE_RESOURCE_STRINGID(AvailableArguments); WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommands); + WINGET_DEFINE_RESOURCE_STRINGID(AvailableHeader); WINGET_DEFINE_RESOURCE_STRINGID(AvailableOptions); WINGET_DEFINE_RESOURCE_STRINGID(AvailableSubcommands); WINGET_DEFINE_RESOURCE_STRINGID(BothManifestAndSearchQueryProvided); @@ -87,6 +88,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(LanguageArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreement); WINGET_DEFINE_RESOURCE_STRINGID(Links); + WINGET_DEFINE_RESOURCE_STRINGID(ListCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(LocationArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LogArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MainCopyrightNotice); @@ -115,7 +118,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); - WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailed); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined); @@ -172,6 +174,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdatedNever); WINGET_DEFINE_RESOURCE_STRINGID(SourceListValue); WINGET_DEFINE_RESOURCE_STRINGID(SourceNameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenFailedSuggestion); + WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenPredefinedFailedSuggestion); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); diff --git a/src/AppInstallerCLICore/TableOutput.h b/src/AppInstallerCLICore/TableOutput.h @@ -30,6 +30,7 @@ namespace AppInstaller::CLI::Execution } // Enables output data in a table format. + // TODO: Improve for use with sparse data. template <size_t FieldCount> struct TableOutput { diff --git a/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp b/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp @@ -57,7 +57,7 @@ namespace AppInstaller::CLI::Workflow { if (searchResult.Matches[i].MatchCriteria.Value.empty()) { - OutputCompletionString(stream, searchResult.Matches[i].Package->GetLatestAvailableVersion()->GetProperty(Repository::PackageVersionProperty::Id)); + OutputCompletionString(stream, searchResult.Matches[i].Package->GetProperty(Repository::PackageProperty::Id)); } else { @@ -107,8 +107,27 @@ namespace AppInstaller::CLI::Workflow switch (m_type) { case Execution::Args::Type::Query: + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: + case Execution::Args::Type::Version: + case Execution::Args::Type::Channel: + context << + Workflow::OpenSource; + break; + } + + context << CompleteWithSingleSemanticsForValueUsingExistingSource(m_type); + } + + void CompleteWithSingleSemanticsForValueUsingExistingSource::operator()(Execution::Context& context) const + { + switch (m_type) + { + case Execution::Args::Type::Query: context << - Workflow::OpenSource << Workflow::RequireCompletionWordNonEmpty << Workflow::SearchSourceForSingleCompletion << Workflow::CompleteWithMatchedField; @@ -118,38 +137,32 @@ namespace AppInstaller::CLI::Workflow break; case Execution::Args::Type::Id: context << - Workflow::OpenSource << Workflow::SearchSourceForCompletionField(Repository::PackageMatchField::Id) << Workflow::CompleteWithMatchedField; break; case Execution::Args::Type::Name: context << - Workflow::OpenSource << Workflow::SearchSourceForCompletionField(Repository::PackageMatchField::Name) << Workflow::CompleteWithMatchedField; break; case Execution::Args::Type::Moniker: context << - Workflow::OpenSource << Workflow::SearchSourceForCompletionField(Repository::PackageMatchField::Moniker) << Workflow::CompleteWithMatchedField; break; case Execution::Args::Type::Tag: context << - Workflow::OpenSource << Workflow::SearchSourceForCompletionField(Repository::PackageMatchField::Tag) << Workflow::CompleteWithMatchedField; break; case Execution::Args::Type::Command: context << - Workflow::OpenSource << Workflow::SearchSourceForCompletionField(Repository::PackageMatchField::Command) << Workflow::CompleteWithMatchedField; break; case Execution::Args::Type::Version: // Here we require that the standard search finds a single entry, and we list those versions. context << - Workflow::OpenSource << Workflow::SearchSourceForSingle << Workflow::EnsureOneMatchFromSearchResult << Workflow::CompleteWithSearchResultVersions; @@ -157,7 +170,6 @@ namespace AppInstaller::CLI::Workflow case Execution::Args::Type::Channel: // Here we require that the standard search finds a single entry, and we list those channels. context << - Workflow::OpenSource << Workflow::SearchSourceForSingle << Workflow::EnsureOneMatchFromSearchResult << Workflow::CompleteWithSearchResultChannels; diff --git a/src/AppInstallerCLICore/Workflows/CompletionFlow.h b/src/AppInstallerCLICore/Workflows/CompletionFlow.h @@ -52,6 +52,21 @@ namespace AppInstaller::CLI::Workflow Execution::Args::Type m_type; }; + // Executes the appropriate completion flow for the given argument in the context of a command + // that targets a single manifest (ex. show or install), using the already open source. + // Required Args: None + // Inputs: CompletionData, Source + // Outputs: None + struct CompleteWithSingleSemanticsForValueUsingExistingSource : public WorkflowTask + { + CompleteWithSingleSemanticsForValueUsingExistingSource(Execution::Args::Type type) : WorkflowTask("CompleteWithSingleSemanticsForValueUsingExistingSource"), m_type(type) {} + + void operator()(Execution::Context& context) const override; + + private: + Execution::Args::Type m_type; + }; + // Outputs an empty line to indicate that there are no completions. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -10,6 +10,8 @@ namespace AppInstaller::CLI::Workflow { + using namespace std::string_literals; + using namespace AppInstaller::Utility::literals; using namespace AppInstaller::Repository; namespace @@ -107,7 +109,7 @@ namespace AppInstaller::CLI::Workflow } catch (...) { - context.Reporter.Error() << Resource::String::OpenSourceFailed << std::endl; + context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; throw; } @@ -122,7 +124,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; for (const auto& details : sources) { - context.Reporter.Info() << " " << details.Name << std::endl; + context.Reporter.Info() << " "_liv << details.Name << std::endl; } AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); @@ -140,16 +142,38 @@ namespace AppInstaller::CLI::Workflow } } - void GetCompositeSourceFromInstalledAndAvailable(Execution::Context& context) + void OpenPredefinedSource::operator()(Execution::Context& context) const { + std::shared_ptr<Repository::ISource> source; + try + { + source = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenPredefinedSource, m_predefinedSource, std::placeholders::_1), true); + } + catch (...) + { + context.Reporter.Error() << Resource::String::SourceOpenPredefinedFailedSuggestion << std::endl; + throw; + } + + // A well known predefined source should return a value. + THROW_HR_IF(E_UNEXPECTED, !source); + + context.Add<Execution::Data::Source>(std::move(source)); + } + + void OpenCompositeSource::operator()(Execution::Context& context) const + { + // Get the already open source for use as the available. std::shared_ptr<Repository::ISource> availableSource = context.Get<Execution::Data::Source>(); - std::shared_ptr<Repository::ISource> installedSource = context.Reporter.ExecuteWithProgress( - std::bind(Repository::OpenPredefinedSource, PredefinedSource::Installed, std::placeholders::_1), true); + // Open the predefined source. + context << OpenPredefinedSource(m_predefinedSource); - std::shared_ptr<Repository::ISource> source = CreateCompositeSource(installedSource, availableSource); + // Create the composite source from the two. + std::shared_ptr<Repository::ISource> compositeSource = Repository::CreateCompositeSource(context.Get<Execution::Data::Source>(), availableSource); - context.Add<Execution::Data::Source>(std::move(source)); + // Overwrite the source with the composite. + context.Add<Execution::Data::Source>(std::move(compositeSource)); } void SearchSourceForMany(Execution::Context& context) @@ -198,6 +222,11 @@ namespace AppInstaller::CLI::Workflow if (args.Contains(Execution::Args::Type::Query)) { std::string_view query = args.GetArg(Execution::Args::Type::Query); + + // Regardless of match type, always use an exact match for the system reference strings. + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, query)); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, query)); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, query)); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, matchType, query)); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); @@ -265,7 +294,15 @@ namespace AppInstaller::CLI::Workflow auto& searchResult = context.Get<Execution::Data::SearchResult>(); Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); - Execution::TableOutput<5> table(context.Reporter, { Resource::String::SearchName, Resource::String::SearchId, Resource::String::SearchVersion, Resource::String::SearchMatch, Resource::String::SearchSource }); + bool sourceIsComposite = context.Get<Execution::Data::Source>()->IsComposite(); + Execution::TableOutput<5> table(context.Reporter, + { + Resource::String::SearchName, + Resource::String::SearchId, + Resource::String::SearchVersion, + Resource::String::SearchMatch, + Resource::String::SearchSource + }); for (size_t i = 0; i < searchResult.Matches.size(); ++i) { @@ -276,8 +313,60 @@ namespace AppInstaller::CLI::Workflow latestVersion->GetProperty(PackageVersionProperty::Id), latestVersion->GetProperty(PackageVersionProperty::Version), GetMatchCriteriaDescriptor(searchResult.Matches[i]), - searchResult.Matches[i].SourceName + sourceIsComposite ? static_cast<std::string>(latestVersion->GetProperty(PackageVersionProperty::SourceName)) : ""s + }); + } + + table.Complete(); + + if (searchResult.Truncated) + { + context.Reporter.Info() << '<' << Resource::String::SearchTruncated << '>' << std::endl; + } + } + + void ReportListResult::operator()(Execution::Context& context) const + { + auto& searchResult = context.Get<Execution::Data::SearchResult>(); + + Execution::TableOutput<5> table(context.Reporter, + { + Resource::String::SearchName, + Resource::String::SearchId, + Resource::String::SearchVersion, + Resource::String::AvailableHeader, + Resource::String::SearchSource }); + + for (const auto& match : searchResult.Matches) + { + auto installedVersion = match.Package->GetInstalledVersion(); + + if (installedVersion) + { + auto latestVersion = match.Package->GetLatestAvailableVersion(); + bool updateAvailable = match.Package->IsUpdateAvailable(); + + // The only time we don't want to output a line is when filtering and no update is available. + if (updateAvailable || !m_onlyShowUpgrades) + { + Utility::LocIndString availableVersion, sourceName; + + if (updateAvailable) + { + availableVersion = latestVersion->GetProperty(PackageVersionProperty::Version); + sourceName = latestVersion->GetProperty(PackageVersionProperty::SourceName); + } + + table.OutputLine({ + match.Package->GetProperty(PackageProperty::Name), + match.Package->GetProperty(PackageProperty::Id), + installedVersion->GetProperty(PackageVersionProperty::Version), + availableVersion, + sourceName + }); + } + } } table.Complete(); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -52,6 +52,34 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source void OpenSource(Execution::Context& context); + // Creates a source object for a predefined source. + // Required Args: None + // Inputs: None + // Outputs: Source + struct OpenPredefinedSource : public WorkflowTask + { + OpenPredefinedSource(Repository::PredefinedSource source) : WorkflowTask("OpenPredefinedSource"), m_predefinedSource(source) {} + + void operator()(Execution::Context& context) const override; + + private: + Repository::PredefinedSource m_predefinedSource; + }; + + // Creates a composite source from the given predefined source and the existing source. + // Required Args: None + // Inputs: Source + // Outputs: Source + struct OpenCompositeSource : public WorkflowTask + { + OpenCompositeSource(Repository::PredefinedSource source) : WorkflowTask("OpenCompositeSource"), m_predefinedSource(source) {} + + void operator()(Execution::Context& context) const override; + + private: + Repository::PredefinedSource m_predefinedSource; + }; + // Performs a search on the source. // Required Args: None // Inputs: Source @@ -98,6 +126,20 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportSearchResult(Execution::Context& context); + // Outputs the search results as the list command would show. + // Required Args: None + // Inputs: SearchResult + // Outputs: None + struct ReportListResult : public WorkflowTask + { + ReportListResult(bool onlyShowUpgrades = false) : WorkflowTask("ReportListResult"), m_onlyShowUpgrades(onlyShowUpgrades) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_onlyShowUpgrades; + }; + // Ensures that there is at least one result in the search. // Required Args: None // Inputs: SearchResult @@ -180,12 +222,6 @@ namespace AppInstaller::CLI::Workflow Settings::ExperimentalFeature::Feature m_feature; }; - // Create a composite source from installed source and available source. - // Required Args: None - // Inputs: Source - // Outputs: Source - void GetCompositeSourceFromInstalledAndAvailable(Execution::Context& context); - // Performs a search on the source with the semantics of targeting packages matching input manifest // Required Args: None // Inputs: Source, Manifest diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -130,6 +130,10 @@ <value>The following commands are available:</value> <comment>Commands the tool supports</comment> </data> + <data name="AvailableHeader" xml:space="preserve"> + <value>Available</value> + <comment>As in "a new version is available to update to".</comment> + </data> <data name="AvailableOptions" xml:space="preserve"> <value>The following options are available:</value> </data> @@ -304,6 +308,13 @@ They can be configured through the settings file 'winget settings'.</value> <value>Links</value> <comment>Links to different webpages</comment> </data> + <data name="ListCommandLongDescription" xml:space="preserve"> + <value>The list command displays the packages installed on the system, as well as whether an update is available. Additional options can be provided to filter the output, much like the search command.</value> + <comment>{Locked="list","search"}</comment> + </data> + <data name="ListCommandShortDescription" xml:space="preserve"> + <value>Display installed packages</value> + </data> <data name="LocationArgumentDescription" xml:space="preserve"> <value>Location to install to (if supported)</value> </data> @@ -426,7 +437,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Progress display as the default color</value> </data> <data name="SearchCommandLongDescription" xml:space="preserve"> - <value>Searches for pacakges from configured sources.</value> + <value>Searches for packages from configured sources.</value> </data> <data name="SearchCommandShortDescription" xml:space="preserve"> <value>Find and show basic info of packages</value> @@ -557,6 +568,14 @@ They can be configured through the settings file 'winget settings'.</value> <data name="SourceNameArgumentDescription" xml:space="preserve"> <value>Name of the source</value> </data> + <data name="SourceOpenFailedSuggestion" xml:space="preserve"> + <value>Failed to open the source; try the 'source reset' command if the problem persists.</value> + <comment>{Locked="source reset"}</comment> + </data> + <data name="SourceOpenPredefinedFailedSuggestion" xml:space="preserve"> + <value>Failed to open the predefined source; please report to winget maintainers.</value> + <comment>{Locked="winget"}</comment> + </data> <data name="SourceRemoveAll" xml:space="preserve"> <value>Removing all sources...</value> </data> @@ -677,9 +696,6 @@ They can be configured through the settings file 'winget settings'.</value> <data name="GetManifestResultVersionNotFound" xml:space="preserve"> <value>No version found matching:</value> </data> - <data name="OpenSourceFailed" xml:space="preserve"> - <value>Failed to open the source; try removing and re-adding it</value> - </data> <data name="OpenSourceFailedNoMatch" xml:space="preserve"> <value>No sources match the given value:</value> </data> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -175,17 +175,21 @@ <ClInclude Include="pch.h" /> <ClInclude Include="TestCommon.h" /> <ClInclude Include="TestHooks.h" /> + <ClInclude Include="TestSource.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="Command.cpp" /> <ClCompile Include="Completion.cpp" /> + <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="Downloader.cpp" /> <ClCompile Include="ExperimentalFeature.cpp" /> <ClCompile Include="HashCommand.cpp" /> <ClCompile Include="MsixInfo.cpp" /> + <ClCompile Include="PredefinedInstalledSource.cpp" /> <ClCompile Include="PreIndexedPackageSource.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> + <ClCompile Include="TestSource.cpp" /> <ClCompile Include="UserSettings.cpp" /> <ClCompile Include="Versions.cpp" /> <ClCompile Include="WorkFlow.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -27,6 +27,9 @@ <ClInclude Include="TestHooks.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="TestSource.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -95,6 +98,15 @@ <ClCompile Include="Completion.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="PredefinedInstalledSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="CompositeSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="TestSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp @@ -0,0 +1,563 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSource.h" +#include <CompositeSource.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; + +constexpr std::string_view s_Everything_Query = "everything"sv; + +// A test source that has two modes: +// 1. A request that IsForEverything returns the stored result. This models the +// incoming search request to a CompositeSource. +// 2. A request that is not for everything invokes TestSource::SearchFunction to +// enable verification of expectations. +struct ComponentTestSource : public TestSource +{ + SearchResult Search(const SearchRequest& request) const override + { + if (request.Query && request.Query.value().Value == s_Everything_Query) + { + return Everything; + } + else if (SearchFunction) + { + return SearchFunction(request); + } + else + { + return {}; + } + } + + SearchResult Everything; +}; + +// A helper to create the sources used by the majority of tests in this file. +struct CompositeTestSeup +{ + CompositeTestSeup() : Composite("*Tests") + { + Installed = std::make_shared<ComponentTestSource>(); + Available = std::make_shared<ComponentTestSource>(); + Composite.SetInstalledSource(Installed); + Composite.AddAvailableSource(Available); + } + + SearchResult Search() + { + SearchRequest request; + request.Query = RequestMatch(MatchType::Exact, s_Everything_Query); + return Composite.Search(request); + } + + std::shared_ptr<ComponentTestSource> Installed; + std::shared_ptr<ComponentTestSource> Available; + CompositeSource Composite; +}; + +// A helper to make matches. +struct Criteria : public PackageMatchFilter +{ + Criteria() : PackageMatchFilter(PackageMatchField::Id, MatchType::Wildcard, ""sv) {} +}; + +Manifest::Manifest MakeDefaultManifest() +{ + Manifest::Manifest result; + + result.Id = "Id"; + result.Name = "Name"; + result.Publisher = "Publisher"; + result.Version = "1.0"; + result.Installers.push_back({}); + + return result; +} + +std::shared_ptr<TestPackage> MakeInstalled(std::function<void(Manifest::Manifest&)> op) +{ + Manifest::Manifest manifest = MakeDefaultManifest(); + op(manifest); + return TestPackage::Make(manifest, TestPackage::InstallationMetadataMap{}); +} + +std::shared_ptr<TestPackage> MakeAvailable(std::function<void(Manifest::Manifest&)> op) +{ + Manifest::Manifest manifest = MakeDefaultManifest(); + op(manifest); + return TestPackage::Make(std::vector<Manifest::Manifest>{ manifest }); +} + +std::function<void(Manifest::Manifest&)> WithPFN(const std::string& pfn) +{ + return [pfn](Manifest::Manifest& m) { m.Installers[0].PackageFamilyName = pfn; }; +} + +std::function<void(Manifest::Manifest&)> WithPC(const std::string& pc) +{ + return [pc](Manifest::Manifest& m) { m.Installers[0].ProductCode = pc; }; +} + +TEST_CASE("CompositeSource_PackageFamilyName_NotAvailable", "[CompositeSource]") +{ + // Pre-folded for easier == + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().empty()); +} + +TEST_CASE("CompositeSource_PackageFamilyName_Available", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); +} + +TEST_CASE("CompositeSource_ProductCode_NotAvailable", "[CompositeSource]") +{ + std::string pc = "thiscouldbeapc"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPC(pc)), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().empty()); +} + +TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") +{ + std::string pc = "thiscouldbeapc"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPC(pc)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pc); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPC(pc)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); +} + +TEST_CASE("CompositeSource_MultiMatch_FindsId", "[CompositeSource]") +{ + std::string name = "MatchingName"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN("sortof_apfn")), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(MakeAvailable([](Manifest::Manifest& m) { m.Id = "A different ID"; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = name; }), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + 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()); +} + +TEST_CASE("CompositeSource_MultiMatch_DoesNotFindId", "[CompositeSource]") +{ + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN("sortof_apfn")), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(MakeAvailable([](Manifest::Manifest& m) { m.Id = "A different ID"; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Id = "Another diff ID"; }), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); + REQUIRE(Version(result.Matches[0].Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Version)).IsUnknown()); +} + +TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + setup.Installed->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); +} + +TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Installed->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); +} + +TEST_CASE("CompositeSource_FoundByAvailableRootSearch_NotInstalled", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.empty()); +} + +TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + MatchType originalType = MatchType::Wildcard; + MatchType type = MatchType::Exact; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); + REQUIRE(result.Matches[0].MatchCriteria.Type == originalType); + + // Now make the source root search find it with a better criteria + setup.Installed->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), PackageMatchFilter(PackageMatchField::Id, type, ""sv)); + + result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); + REQUIRE(result.Matches[0].MatchCriteria.Type == type); +} + +TEST_CASE("CompositePackage_PropertyFromInstalled", "[CompositeSource]") +{ + std::string id = "Special test ID"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled([&](Manifest::Manifest& m) { m.Id = id; }), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetProperty(PackageProperty::Id) == id); +} + +TEST_CASE("CompositePackage_PropertyFromAvailable", "[CompositeSource]") +{ + std::string id = "Special test ID"; + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Id = id; }), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetProperty(PackageProperty::Id) == id); +} + +TEST_CASE("CompositePackage_AvailableVersions_ChannelFilteredOut", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + std::string channel = "Channel"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + Manifest::Manifest noChannel = MakeDefaultManifest(); + noChannel.Version = "1.0"; + + Manifest::Manifest hasChannel = MakeDefaultManifest(); + hasChannel.Channel = channel; + hasChannel.Version = "2.0"; + + SearchResult result; + result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }), Criteria()); + REQUIRE(result.Matches.back().Package->GetAvailableVersionKeys().size() == 2); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto versionKeys = result.Matches[0].Package->GetAvailableVersionKeys(); + REQUIRE(versionKeys.size() == 1); + REQUIRE(versionKeys[0].Channel.empty()); + + auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(); + REQUIRE(latestVersion); + REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get().empty()); + + REQUIRE(!result.Matches[0].Package->IsUpdateAvailable()); +} + +TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + std::string channel = "Channel"; + + CompositeTestSeup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled([&](Manifest::Manifest& m) { m.Installers[0].PackageFamilyName = pfn; m.Channel = channel; }), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + Manifest::Manifest noChannel = MakeDefaultManifest(); + noChannel.Version = "1.0"; + + Manifest::Manifest hasChannel = MakeDefaultManifest(); + hasChannel.Channel = channel; + hasChannel.Version = "2.0"; + + SearchResult result; + result.Matches.emplace_back(TestPackage::Make(std::vector<Manifest::Manifest>{ noChannel, hasChannel }), Criteria()); + REQUIRE(result.Matches.back().Package->GetAvailableVersionKeys().size() == 2); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto versionKeys = result.Matches[0].Package->GetAvailableVersionKeys(); + REQUIRE(versionKeys.size() == 1); + REQUIRE(versionKeys[0].Channel == channel); + + auto latestVersion = result.Matches[0].Package->GetLatestAvailableVersion(); + REQUIRE(latestVersion); + REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get() == channel); + + REQUIRE(result.Matches[0].Package->IsUpdateAvailable()); +} + +TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + std::string firstName = "Name1"; + std::string secondName = "Name2"; + + CompositeTestSeup setup; + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); + setup.Composite.AddAvailableSource(secondAvailable); + + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = firstName; }), Criteria()); + return result; + }; + + secondAvailable->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + 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); +} + +TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + std::string firstName = "Name1"; + std::string secondName = "Name2"; + + CompositeTestSeup setup; + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); + setup.Composite.AddAvailableSource(secondAvailable); + + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + + secondAvailable->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + 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); +} + +TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[CompositeSource]") +{ + std::string pfn = "sortof_apfn"; + + CompositeTestSeup setup; + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); + setup.Composite.AddAvailableSource(secondAvailable); + + setup.Installed->SearchFunction = [&](const SearchRequest& request) + { + REQUIRE(request.Inclusions.size() == 1); + REQUIRE(request.Inclusions[0].Value == pfn); + + SearchResult result; + result.Matches.emplace_back(MakeInstalled(WithPFN(pfn)), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + secondAvailable->Everything.Matches.emplace_back(MakeAvailable(WithPFN(pfn)), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Package->GetInstalledVersion()); + REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); +} diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerRepositorySource.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerStrings.h> +#include <Microsoft/PredefinedInstalledSourceFactory.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Runtime; +using namespace AppInstaller::Utility; + +using Factory = AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory; + +std::shared_ptr<ISource> CreatePredefinedInstalledSource(Factory::Filter filter = Factory::Filter::None) +{ + SourceDetails details; + details.Type = Factory::Type(); + details.Arg = Factory::FilterToString(filter); + + TestProgress progress; + + auto factory = Factory::Create(); + return factory->Create(details, progress); +} + +TEST_CASE("PredefinedInstalledSource_Create", "[installed][list]") +{ + auto source = CreatePredefinedInstalledSource(); +} + +TEST_CASE("PredefinedInstalledSource_Search", "[installed][list]") +{ + auto source = CreatePredefinedInstalledSource(); + + SearchRequest request; + + auto results = source->Search(request); + + REQUIRE(!results.Matches.empty()); +} diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -5,6 +5,7 @@ #include <SQLiteWrapper.h> #include <Microsoft/SQLiteIndex.h> #include <winget/Manifest.h> +#include <AppInstallerStrings.h> #include <Microsoft/Schema/1_0/IdTable.h> #include <Microsoft/Schema/1_0/NameTable.h> @@ -18,6 +19,7 @@ #include <Microsoft/Schema/1_0/SearchResultsTable.h> using namespace std::string_literals; +using namespace std::string_view_literals; using namespace TestCommon; using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; @@ -1863,3 +1865,63 @@ TEST_CASE("SQLiteIndex_CheckConsistency_Failure", "[sqliteindex][V1_1]") REQUIRE(!index.CheckConsistency(true)); } } + +TEST_CASE("SQLiteIndex_GetMultiProperty_PackageFamilyName", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Id1", "Name1", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1", { "PFN1", "PFN2" }, {} }, + }); + + Schema::Version testVersion = TestPrepareForRead(index); + + SearchRequest request; + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 1); + + auto props = index.GetMultiPropertyByManifestId(results.Matches[0].first, PackageVersionMultiProperty::PackageFamilyName); + + if (ArePackageFamilyNameAndProductCodeSupported(index, testVersion)) + { + REQUIRE(props.size() == 2); + REQUIRE(std::find(props.begin(), props.end(), FoldCase("PFN1"sv)) != props.end()); + REQUIRE(std::find(props.begin(), props.end(), FoldCase("PFN2"sv)) != props.end()); + } + else + { + REQUIRE(props.empty()); + } +} + +TEST_CASE("SQLiteIndex_GetMultiProperty_ProductCode", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Id1", "Name1", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1", {}, { "PC1", "PC2" } }, + }); + + Schema::Version testVersion = TestPrepareForRead(index); + + SearchRequest request; + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 1); + + auto props = index.GetMultiPropertyByManifestId(results.Matches[0].first, PackageVersionMultiProperty::ProductCode); + + if (ArePackageFamilyNameAndProductCodeSupported(index, testVersion)) + { + REQUIRE(props.size() == 2); + REQUIRE(std::find(props.begin(), props.end(), FoldCase("PC1"sv)) != props.end()); + REQUIRE(std::find(props.begin(), props.end(), FoldCase("PC2"sv)) != props.end()); + } + else + { + REQUIRE(props.empty()); + } +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include "TestHooks.h" +#include "TestSource.h" #include <AppInstallerRepositorySource.h> #include <AppInstallerDateTime.h> @@ -109,44 +110,31 @@ Sources: namespace { // Helper to create a simple source. - struct TestSource : public ISource + struct SourcesTestSource : public TestCommon::TestSource { - TestSource() = default; - TestSource(const SourceDetails& details) : m_details(details) {} - - static std::shared_ptr<ISource> Create(const SourceDetails& details) - { - // using return std::make_shared<TestSource>(details); will crash the x86 test during destruction. - return std::shared_ptr<ISource>(new TestSource(details)); - } - - // ISource - const SourceDetails& GetDetails() const override + SourcesTestSource() = default; + SourcesTestSource(const SourceDetails& details) { - return m_details; + Details = details; } - const std::string& GetIdentifier() const override + static std::shared_ptr<ISource> Create(const SourceDetails& details) { - return m_identifier; + // using return std::make_shared<TestSource>(details); will crash the x86 test during destruction. + return std::shared_ptr<ISource>(new SourcesTestSource(details)); } - SearchResult Search(const SearchRequest& request) const override + SearchResult Search(const SearchRequest&) const override { - UNREFERENCED_PARAMETER(request); - SearchResult result; PackageMatchFilter testMatchFilter1{ PackageMatchField::Id, MatchType::Exact, "test" }; PackageMatchFilter testMatchFilter2{ PackageMatchField::Name, MatchType::Exact, "test" }; PackageMatchFilter testMatchFilter3{ PackageMatchField::Id, MatchType::CaseInsensitive, "test" }; - result.Matches.emplace_back(std::unique_ptr<IPackage>(), testMatchFilter1); - result.Matches.emplace_back(std::unique_ptr<IPackage>(), testMatchFilter2); - result.Matches.emplace_back(std::unique_ptr<IPackage>(), testMatchFilter3); + result.Matches.emplace_back(std::shared_ptr<IPackage>(), testMatchFilter1); + result.Matches.emplace_back(std::shared_ptr<IPackage>(), testMatchFilter2); + result.Matches.emplace_back(std::shared_ptr<IPackage>(), testMatchFilter3); return result; } - - SourceDetails m_details; - std::string m_identifier = "*TestSource"; }; // Helper that allows some lambdas to be wrapped into a source factory. @@ -158,7 +146,7 @@ namespace using RemoveFunctor = std::function<void(const SourceDetails&)>; TestSourceFactory() : - m_Create(TestSource::Create), m_Add([](SourceDetails&) {}), m_Update([](const SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} + m_Create(SourcesTestSource::Create), m_Add([](SourceDetails&) {}), m_Update([](const SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} // ISourceFactory std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback&) override @@ -590,8 +578,6 @@ TEST_CASE("RepoSources_SearchAcrossMultipleSources", "[sources]") ProgressCallback progress; auto source = OpenSource("", progress); - REQUIRE(source->GetDetails().IsAggregated); - SearchRequest request; auto result = source->Search(request); REQUIRE(result.Matches.size() == 6); diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -8,8 +8,6 @@ #include <functional> #include <string> -#define SQLITE_MEMORY_DB_CONNECTION_TARGET ":memory:" - #define REQUIRE_THROWS_HR(_expr_, _hr_) REQUIRE_THROWS_MATCHES(_expr_, wil::ResultException, ::TestCommon::ResultExceptionHRMatcher(_hr_)) namespace TestCommon diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSource.h" + +using namespace AppInstaller; +using namespace AppInstaller::Repository; + +namespace TestCommon +{ + TestPackageVersion::TestPackageVersion(const Manifest& manifest, InstallationMetadataMap installationMetadata) : + VersionManifest(manifest), InstallationMetadata(std::move(installationMetadata)) {} + + TestPackageVersion::LocIndString TestPackageVersion::GetProperty(PackageVersionProperty property) const + { + switch (property) + { + case PackageVersionProperty::Id: + return LocIndString{ VersionManifest.Id }; + case PackageVersionProperty::Name: + return LocIndString{ VersionManifest.Name }; + case PackageVersionProperty::Version: + return LocIndString{ VersionManifest.Version }; + case PackageVersionProperty::Channel: + return LocIndString{ VersionManifest.Channel }; + default: + return {}; + } + } + + std::vector<TestPackageVersion::LocIndString> TestPackageVersion::GetMultiProperty(PackageVersionMultiProperty property) const + { + std::vector<LocIndString> result; + + switch (property) + { + case PackageVersionMultiProperty::PackageFamilyName: + for (const auto& installer : VersionManifest.Installers) + { + AddFoldedIfHasValueAndNotPresent(installer.PackageFamilyName, result); + } + break; + case PackageVersionMultiProperty::ProductCode: + for (const auto& installer : VersionManifest.Installers) + { + AddFoldedIfHasValueAndNotPresent(installer.ProductCode, result); + } + break; + } + + return result; + } + + TestPackageVersion::Manifest TestPackageVersion::GetManifest() const + { + return VersionManifest; + } + + std::map<std::string, std::string> TestPackageVersion::GetInstallationMetadata() const + { + return InstallationMetadata; + } + + void TestPackageVersion::AddFoldedIfHasValueAndNotPresent(const Utility::NormalizedString& value, std::vector<LocIndString>& target) + { + if (!value.empty()) + { + std::string folded = FoldCase(value); + auto itr = std::find(target.begin(), target.end(), folded); + if (itr == target.end()) + { + target.emplace_back(std::move(folded)); + } + } + } + + TestPackage::TestPackage(const std::vector<Manifest>& available) + { + for (const auto& manifest : available) + { + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + } + } + + TestPackage::TestPackage(const Manifest& installed, InstallationMetadataMap installationMetadata, const std::vector<Manifest>& available) : + InstalledVersion(TestPackageVersion::Make(installed, std::move(installationMetadata))) + { + for (const auto& manifest : available) + { + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + } + } + + TestPackage::LocIndString TestPackage::GetProperty(PackageProperty property) const + { + std::shared_ptr<IPackageVersion> truth; + + if (!AvailableVersions.empty()) + { + truth = AvailableVersions[0]; + } + else + { + truth = InstalledVersion; + } + + if (!truth) + { + THROW_HR(E_NOT_VALID_STATE); + } + + switch (property) + { + case PackageProperty::Id: + return truth->GetProperty(PackageVersionProperty::Id); + case PackageProperty::Name: + return truth->GetProperty(PackageVersionProperty::Name); + default: + return {}; + } + } + + std::shared_ptr<IPackageVersion> TestPackage::GetInstalledVersion() const + { + return InstalledVersion; + } + + std::vector<PackageVersionKey> TestPackage::GetAvailableVersionKeys() const + { + std::vector<PackageVersionKey> result; + for (const auto& version : AvailableVersions) + { + result.emplace_back(PackageVersionKey("", version->GetProperty(PackageVersionProperty::Version).get(), version->GetProperty(PackageVersionProperty::Channel).get())); + } + return result; + } + + std::shared_ptr<IPackageVersion> TestPackage::GetLatestAvailableVersion() const + { + if (AvailableVersions.empty()) + { + return {}; + } + + return AvailableVersions[0]; + } + + std::shared_ptr<IPackageVersion> TestPackage::GetAvailableVersion(const PackageVersionKey& versionKey) const + { + for (const auto& version : AvailableVersions) + { + if ((versionKey.Version.empty() || versionKey.Version == version->GetProperty(PackageVersionProperty::Version).get()) && + (versionKey.Channel.empty() || versionKey.Channel == version->GetProperty(PackageVersionProperty::Channel).get())) + { + return version; + } + } + + return {}; + } + + bool TestPackage::IsUpdateAvailable() const + { + if (InstalledVersion && !AvailableVersions.empty()) + { + Utility::Version installed{ InstalledVersion->GetProperty(PackageVersionProperty::Version) }; + Utility::Version available{ AvailableVersions[0]->GetProperty(PackageVersionProperty::Version) }; + + return available > installed; + } + + return false; + } + + const SourceDetails& TestSource::GetDetails() const + { + return Details; + } + + const std::string& TestSource::GetIdentifier() const + { + return Identifier; + } + + SearchResult TestSource::Search(const SearchRequest& request) const + { + if (SearchFunction) + { + return SearchFunction(request); + } + else + { + return {}; + } + } + + bool TestSource::IsComposite() const + { + return Composite; + } +} diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <Public/AppInstallerRepositorySource.h> +#include <winget/Manifest.h> + +#include <functional> +#include <utility> + +namespace TestCommon +{ + // IPackageVersion for TestSource + struct TestPackageVersion : public AppInstaller::Repository::IPackageVersion + { + using Manifest = AppInstaller::Manifest::Manifest; + using LocIndString = AppInstaller::Utility::LocIndString; + using InstallationMetadataMap = std::map<std::string, std::string>; + + TestPackageVersion(const Manifest& manifest, InstallationMetadataMap installationMetadata = {}); + + template <typename... Args> + static std::shared_ptr<TestPackageVersion> Make(Args&&... args) + { + return std::make_shared<TestPackageVersion>(std::forward<Args>(args)...); + } + + LocIndString GetProperty(AppInstaller::Repository::PackageVersionProperty property) const override; + std::vector<LocIndString> GetMultiProperty(AppInstaller::Repository::PackageVersionMultiProperty property) const override; + Manifest GetManifest() const override; + InstallationMetadataMap GetInstallationMetadata() const override; + + Manifest VersionManifest; + InstallationMetadataMap InstallationMetadata; + + protected: + static void AddFoldedIfHasValueAndNotPresent(const AppInstaller::Utility::NormalizedString& value, std::vector<LocIndString>& target); + }; + + // IPackage for TestSource + struct TestPackage : public AppInstaller::Repository::IPackage + { + using Manifest = AppInstaller::Manifest::Manifest; + using LocIndString = AppInstaller::Utility::LocIndString; + using InstallationMetadataMap = TestPackageVersion::InstallationMetadataMap; + + // Create a package with only available versions using these manifests. + TestPackage(const std::vector<Manifest>& available); + + // Create a package with an installed version, metadata, and optionally available versions. + TestPackage(const Manifest& installed, InstallationMetadataMap installationMetadata, const std::vector<Manifest>& available = {}); + + template <typename... Args> + static std::shared_ptr<TestPackage> Make(Args&&... args) + { + return std::make_shared<TestPackage>(std::forward<Args>(args)...); + } + + AppInstaller::Utility::LocIndString GetProperty(AppInstaller::Repository::PackageProperty property) const override; + std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetInstalledVersion() const override; + std::vector<AppInstaller::Repository::PackageVersionKey> GetAvailableVersionKeys() const override; + std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetLatestAvailableVersion() const override; + std::shared_ptr<AppInstaller::Repository::IPackageVersion> GetAvailableVersion(const AppInstaller::Repository::PackageVersionKey& versionKey) const override; + bool IsUpdateAvailable() const override; + + std::shared_ptr<AppInstaller::Repository::IPackageVersion> InstalledVersion; + std::vector<std::shared_ptr<AppInstaller::Repository::IPackageVersion>> AvailableVersions; + }; + + // An ISource implementation for use across the test code. + struct TestSource : public AppInstaller::Repository::ISource + { + const AppInstaller::Repository::SourceDetails& GetDetails() const override; + const std::string& GetIdentifier() const override; + AppInstaller::Repository::SearchResult Search(const AppInstaller::Repository::SearchRequest& request) const override; + bool IsComposite() const override; + + AppInstaller::Repository::SourceDetails Details; + std::string Identifier = "*TestSource"; + std::function<AppInstaller::Repository::SearchResult(const AppInstaller::Repository::SearchRequest& request)> SearchFunction; + bool Composite = false; + }; +} diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp @@ -159,3 +159,35 @@ TEST_CASE("MinOsVersion_Check", "[versions]") REQUIRE(Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version("6.1"))); REQUIRE(!Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version("10.0.65535"))); } + +TEST_CASE("VersionLatest", "[versions]") +{ + REQUIRE(Version::CreateLatest().IsLatest()); + REQUIRE(Version("latest").IsLatest()); + REQUIRE(Version("LATEST").IsLatest()); + REQUIRE(!Version("1.0").IsLatest()); + + RequireLessThan("1.0", "latest"); + RequireLessThan("100", "latest"); + RequireLessThan("943849587389754876.1", "latest"); + + RequireEqual("latest", "LATEST"); +} + +TEST_CASE("VersionUnknown", "[versions]") +{ + REQUIRE(Version::CreateUnknown().IsUnknown()); + REQUIRE(Version("unknown").IsUnknown()); + REQUIRE(Version("UNKNOWN").IsUnknown()); + REQUIRE(!Version("1.0").IsUnknown()); + + RequireLessThan("unknown", "1.0"); + RequireLessThan("unknown", "1.fork"); + + RequireEqual("unknown", "UNKNOWN"); +} + +TEST_CASE("VersionUnknownLessThanLatest", "[versions]") +{ + REQUIRE(Version::CreateUnknown() < Version::CreateLatest()); +} diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestSource.h" #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> #include <AppInstallerDownloader.h> @@ -38,102 +39,7 @@ using namespace AppInstaller::Utility; namespace { - struct TestPackageVersion : public IPackageVersion - { - TestPackageVersion(const Manifest& manifest, std::map<std::string, std::string> installationMetadata = {}) : - m_manifest(manifest), m_installationMetadata(std::move(installationMetadata)) {} - - LocIndString GetProperty(PackageVersionProperty property) const override - { - switch (property) - { - case PackageVersionProperty::Id: - return LocIndString{ m_manifest.Id }; - case PackageVersionProperty::Name: - return LocIndString{ m_manifest.Name }; - case PackageVersionProperty::Version: - return LocIndString{ m_manifest.Version }; - case PackageVersionProperty::Channel: - return LocIndString{ m_manifest.Channel }; - default: - return {}; - } - } - - Manifest GetManifest() const override - { - return m_manifest; - } - - std::map<std::string, std::string> GetInstallationMetadata() const override - { - return m_installationMetadata; - } - - Manifest m_manifest; - std::map<std::string, std::string> m_installationMetadata; - }; - - struct TestPackage : public IPackage - { - // The input manifest list should have been sorted, GetAvailableVersions will just return in the order of input manifest list.. - // installedIndex is the index of the manifest in the list to be returned by GetInstalledVersion(), - // -1 mean no installed version - TestPackage(std::vector<Manifest> manifestList, int installedIndex = -1, std::map<std::string, std::string> installationMetadata = {}) : - m_manifestList(manifestList), m_installedIndex(installedIndex), m_installationMetadata(installationMetadata){} - - std::shared_ptr<IPackageVersion> GetInstalledVersion() const override - { - if (m_installedIndex >= 0) - { - return std::make_shared<TestPackageVersion>(m_manifestList.at(m_installedIndex), m_installationMetadata); - } - else - { - return {}; - } - } - - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override - { - std::vector<PackageVersionKey> result; - for (const auto& manifest : m_manifestList) - { - result.emplace_back(PackageVersionKey("", manifest.Version, manifest.Channel)); - } - return result; - } - - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override - { - return std::make_shared<TestPackageVersion>(m_manifestList.at(0)); - } - - std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override - { - for (const auto& manifest : m_manifestList) - { - if ((versionKey.Version.empty() || versionKey.Version == manifest.Version) && - (versionKey.Channel.empty() || versionKey.Channel == manifest.Channel)) - { - return std::make_shared<TestPackageVersion>(manifest); - } - } - - return {}; - } - - bool IsUpdateAvailable() const override - { - return false; - } - - std::vector<Manifest> m_manifestList; - int m_installedIndex; - std::map<std::string, std::string> m_installationMetadata; - }; - - struct TestSource : public ISource + struct WorkflowTestSource : public TestSource { SearchResult Search(const SearchRequest& request) const override { @@ -155,7 +61,7 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }), + TestPackage::Make(std::vector<Manifest>{ manifest }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } else if (input == "TestQueryReturnTwo") @@ -163,25 +69,21 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }), + TestPackage::Make(std::vector<Manifest>{ manifest }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2 }), + TestPackage::Make(std::vector<Manifest>{ manifest2 }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); } return result; } - - const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } - - const std::string& GetIdentifier() const override { THROW_HR(E_NOTIMPL); } }; - struct TestCompositeInstalledSource : public ISource + struct WorkflowTestCompositeSource : public TestSource { SearchResult Search(const SearchRequest& request) const override { @@ -205,8 +107,11 @@ namespace auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, - std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }), + TestPackage::Make( + manifest, + TestPackage::InstallationMetadataMap{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }, + std::vector<Manifest>{ manifest2, manifest } + ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -216,8 +121,11 @@ namespace auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Msix.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, - std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }), + TestPackage::Make( + manifest, + TestPackage::InstallationMetadataMap{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }, + std::vector<Manifest>{ manifest2, manifest } + ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); } @@ -226,8 +134,11 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_MSStore.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }, 0, - std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "MSStore" } }), + TestPackage::Make( + manifest, + TestPackage::InstallationMetadataMap{ { s_InstallationMetadata_Key_InstallerType, "MSStore" } }, + std::vector<Manifest>{ manifest } + ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); } @@ -237,8 +148,11 @@ namespace auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 0, - std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }), + TestPackage::Make( + manifest2, + TestPackage::InstallationMetadataMap{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }, + std::vector<Manifest>{ manifest2, manifest } + ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -248,17 +162,16 @@ namespace auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, - std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }), + TestPackage::Make( + manifest, + TestPackage::InstallationMetadataMap{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }, + std::vector<Manifest>{ manifest2, manifest } + ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } return result; } - - const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } - - const std::string& GetIdentifier() const override { THROW_HR(E_NOTIMPL); } }; struct TestContext; @@ -356,7 +269,7 @@ void OverrideForOpenSource(TestContext& context) { context.Override({ Workflow::OpenSource, [](TestContext& context) { - context.Add<Execution::Data::Source>(std::make_shared<TestSource>()); + context.Add<Execution::Data::Source>(std::make_shared<WorkflowTestSource>()); } }); } @@ -366,9 +279,9 @@ void OverrideForCompositeInstalledSource(TestContext& context) { } }); - context.Override({ Workflow::GetCompositeSourceFromInstalledAndAvailable, [](TestContext& context) + context.Override({ "OpenCompositeSource", [](TestContext& context) { - context.Add<Execution::Data::Source>(std::make_shared<TestCompositeInstalledSource>()); + context.Add<Execution::Data::Source>(std::make_shared<WorkflowTestCompositeSource>()); } }); } @@ -440,7 +353,7 @@ void OverrideForMSStore(TestContext& context, bool isUpdate) } }); } -TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") +TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -463,7 +376,7 @@ TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } -TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") +TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -481,7 +394,7 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); } -TEST_CASE("MSStoreInstallFlowWithTestManifest", "[InstallFlow]") +TEST_CASE("MSStoreInstallFlowWithTestManifest", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestMSStoreInstalled.txt"); @@ -503,7 +416,7 @@ TEST_CASE("MSStoreInstallFlowWithTestManifest", "[InstallFlow]") REQUIRE(installResultStr.find("9WZDNCRFJ364") != std::string::npos); } -TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") +TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); @@ -527,7 +440,7 @@ TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") REQUIRE(uri.SchemeName() == L"file"); } -TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") +TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); @@ -551,7 +464,7 @@ TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") REQUIRE(uri.SchemeName() == L"https"); } -TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") +TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow][workflow]") { { std::ostringstream installOutput; @@ -661,7 +574,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") } } -TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow]") +TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -685,7 +598,7 @@ TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow]") REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } -TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow]") +TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow][workflow]") { std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; @@ -700,7 +613,7 @@ TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow]") REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::NoPackageFound).get()) != std::string::npos); } -TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") +TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow][workflow]") { std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; @@ -715,7 +628,7 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::MultiplePackagesFound).get()) != std::string::npos); } -TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow]") +TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") { std::ostringstream showOutput; TestContext context{ showOutput, std::cin }; @@ -733,7 +646,7 @@ TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow]") REQUIRE(showOutput.str().find("https://ThisIsNotUsed") != std::string::npos); } -TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow]") +TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow][workflow]") { std::ostringstream showOutput; TestContext context{ showOutput, std::cin }; @@ -751,7 +664,7 @@ TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow]") REQUIRE(showOutput.str().find(" Download Url: https://ThisIsNotUsed") == std::string::npos); } -TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -775,7 +688,7 @@ TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow]") REQUIRE(updateResultStr.find("/silentwithprogress") != std::string::npos); } -TEST_CASE("UpdateFlow_UpdateWithManifestMSStore", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateWithManifestMSStore", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestMSStoreUpdated.txt"); @@ -798,7 +711,7 @@ TEST_CASE("UpdateFlow_UpdateWithManifestMSStore", "[UpdateFlow]") REQUIRE(updateResultStr.find("9WZDNCRFJ364") != std::string::npos); } -TEST_CASE("UpdateFlow_UpdateWithManifestAppNotInstalled", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateWithManifestAppNotInstalled", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -817,7 +730,7 @@ TEST_CASE("UpdateFlow_UpdateWithManifestAppNotInstalled", "[UpdateFlow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } -TEST_CASE("UpdateFlow_UpdateWithManifestVersionAlreadyInstalled", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateWithManifestVersionAlreadyInstalled", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -836,7 +749,7 @@ TEST_CASE("UpdateFlow_UpdateWithManifestVersionAlreadyInstalled", "[UpdateFlow]" REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } -TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -861,7 +774,7 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow]") REQUIRE(updateResultStr.find("/silence") != std::string::npos); } -TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestMsixInstalled.txt"); @@ -879,7 +792,7 @@ TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow]") REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); } -TEST_CASE("UpdateFlow_UpdateMSStore", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateMSStore", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestMSStoreUpdated.txt"); @@ -902,7 +815,7 @@ TEST_CASE("UpdateFlow_UpdateMSStore", "[UpdateFlow]") REQUIRE(updateResultStr.find("9WZDNCRFJ364") != std::string::npos); } -TEST_CASE("UpdateFlow_UpdateExeLatestAlreadyInstalled", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateExeLatestAlreadyInstalled", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -921,7 +834,7 @@ TEST_CASE("UpdateFlow_UpdateExeLatestAlreadyInstalled", "[UpdateFlow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } -TEST_CASE("UpdateFlow_UpdateExeInstallerTypeNotApplicable", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateExeInstallerTypeNotApplicable", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -940,7 +853,7 @@ TEST_CASE("UpdateFlow_UpdateExeInstallerTypeNotApplicable", "[UpdateFlow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } -TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotFound", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotFound", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -960,7 +873,7 @@ TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotFound", "[UpdateFlow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } -TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotApplicable", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotApplicable", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -980,7 +893,7 @@ TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotApplicable", "[UpdateFlow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } -TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow]") +TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") { TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -94,8 +94,7 @@ namespace AppInstaller::Utility bool CaseInsensitiveEquals(std::string_view a, std::string_view b) { - // TODO: When we bring in ICU, do this correctly. - return ToLower(a) == ToLower(b); + return FoldCase(a) == FoldCase(b); } bool CaseInsensitiveStartsWith(std::string_view a, std::string_view b) diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -21,6 +21,8 @@ namespace AppInstaller::Settings return User().Get<Setting::EFExperimentalArg>(); case Feature::ExperimentalMSStore: return User().Get<Setting::EFExperimentalMSStore>(); + case Feature::ExperimentalList: + return User().Get<Setting::EFList>(); case Feature::ExperimentalUpgrade: return User().Get<Setting::EFExperimentalUpgrade>(); default: @@ -38,6 +40,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Argument Sample", "experimentalArg", "https://aka.ms/winget-settings", Feature::ExperimentalArg }; case Feature::ExperimentalMSStore: return ExperimentalFeature{ "Microsoft Store Support", "experimentalMSStore", "https://aka.ms/winget-settings", Feature::ExperimentalMSStore }; + case Feature::ExperimentalList: + return ExperimentalFeature{ "List Command", "list", "https://aka.ms/winget-settings", Feature::ExperimentalList }; case Feature::ExperimentalUpgrade: return ExperimentalFeature{ "Upgrade Command", "upgrade", "https://aka.ms/winget-settings", Feature::ExperimentalUpgrade }; default: diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLogging.h b/src/AppInstallerCommonCore/Public/AppInstallerLogging.h @@ -50,6 +50,7 @@ namespace AppInstaller::Logging { Verbose, Info, + Warning, Error, Crit, }; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -46,10 +46,23 @@ namespace AppInstaller::Utility bool operator==(const Version& other) const; bool operator!=(const Version& other) const; + // Determines if this version is the sentinel value defining the 'Latest' version + bool IsLatest() const; + + // Returns a Version that will return true for IsLatest + static Version CreateLatest(); + + // Determines if this version is the sentinel value defining an 'Unknown' version + bool IsUnknown() const; + + // Returns a Version that will return true for IsUnknown + static Version CreateUnknown(); + // An individual version part in between split characters. struct Part { Part(const std::string& part); + Part(uint64_t integer, std::string other); bool operator<(const Part& other) const; bool operator==(const Part& other) const; @@ -72,6 +85,7 @@ namespace AppInstaller::Utility // Compared lexicographically. struct Channel { + Channel() = default; Channel(const std::string& channel) : m_channel(channel) {} Channel(std::string&& channel) : m_channel(std::move(channel)) {} @@ -95,6 +109,7 @@ namespace AppInstaller::Utility // 2.0, "alpha" struct VersionAndChannel { + VersionAndChannel() = default; VersionAndChannel(Version&& version, Channel&& channel); const Version& GetVersion() const { return m_version; } @@ -104,6 +119,9 @@ namespace AppInstaller::Utility bool operator<(const VersionAndChannel& other) const; + // A convenience function to make more sematic sense at call sites over the somewhat awkward less than ordering. + bool IsUpdatedBy(const VersionAndChannel& other) const; + private: Version m_version; Channel m_channel; diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -22,8 +22,9 @@ namespace AppInstaller::Settings ExperimentalCmd = 0x1, ExperimentalArg = 0x2, ExperimentalMSStore = 0x4, - ExperimentalUpgrade = 0x8, - Max = 0x10, // This MUST always be last + ExperimentalList = 0x8, + ExperimentalUpgrade = 0x10, + Max = 0x11, // This MUST always be last }; using Feature_t = std::underlying_type_t<ExperimentalFeature::Feature>; diff --git a/src/AppInstallerCommonCore/Public/winget/LocIndependent.h b/src/AppInstallerCommonCore/Public/winget/LocIndependent.h @@ -49,7 +49,9 @@ namespace AppInstaller::Utility const std::string* operator->() const { return &m_value; } - bool operator==(std::string_view sv) { return m_value == sv; } + bool operator==(std::string_view sv) const { return m_value == sv; } + + bool operator<(const LocIndString& other) const { return m_value < other.m_value; } private: std::string m_value; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -49,6 +49,7 @@ namespace AppInstaller::Settings EFExperimentalCmd, EFExperimentalArg, EFExperimentalMSStore, + EFList, EFExperimentalUpgrade, Max }; @@ -82,8 +83,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.experimentalUpgrade"sv); - + SETTINGMAPPING_SPECIALIZATION(Setting::EFList, bool, bool, false, ".experimentalFeatures.list"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.upgrade"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template <size_t... I> diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -16,6 +16,8 @@ namespace AppInstaller::Settings static constexpr std::string_view s_SettingEmpty = R"({ + "$schema": "https://aka.ms/winget-settings.schema.json", + // For documentation on these settings, see: https://aka.ms/winget-settings // "source": { // "autoUpdateIntervalInMinutes": 5 @@ -197,6 +199,12 @@ namespace AppInstaller::Settings return value; } + std::optional<SettingMapping<Setting::EFList>::value_t> + SettingMapping<Setting::EFList>::Validate(const SettingMapping<Setting::EFList>::json_t& value) + { + return value; + } + std::optional<SettingMapping<Setting::EFExperimentalUpgrade>::value_t> SettingMapping<Setting::EFExperimentalUpgrade>::Validate(const SettingMapping<Setting::EFExperimentalUpgrade>::json_t& value) { diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -2,9 +2,15 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/AppInstallerVersions.h" +#include "Public/AppInstallerStrings.h" namespace AppInstaller::Utility { + using namespace std::string_view_literals; + + static constexpr std::string_view s_Version_Part_Latest = "Latest"sv; + static constexpr std::string_view s_Version_Part_Unknown = "Unknown"sv; + Version::Version(std::string&& version, std::string_view splitChars) { Assign(std::move(version), splitChars); @@ -42,6 +48,26 @@ namespace AppInstaller::Utility bool Version::operator<(const Version& other) const { + // Sort Latest higher than any other values + bool thisIsLatest = IsLatest(); + bool otherIsLatest = other.IsLatest(); + + if (thisIsLatest || otherIsLatest) + { + // If at least one is latest, this can only be less than if the other is and this is not. + return (otherIsLatest && !thisIsLatest); + } + + // Sort Unknown lower than any known values + bool thisIsUnknown = IsUnknown(); + bool otherIsUnknown = other.IsUnknown(); + + if (thisIsUnknown || otherIsUnknown) + { + // If at least one is unknown, this can only be less than if it is and the other is not. + return (thisIsUnknown && !otherIsUnknown); + } + for (size_t i = 0; i < m_parts.size(); ++i) { if (i >= other.m_parts.size()) @@ -85,6 +111,12 @@ namespace AppInstaller::Utility bool Version::operator==(const Version& other) const { + if ((IsLatest() && other.IsLatest()) || + (IsUnknown() && other.IsUnknown())) + { + return true; + } + if (m_parts.size() != other.m_parts.size()) { return false; @@ -106,6 +138,30 @@ namespace AppInstaller::Utility return !(*this == other); } + bool Version::IsLatest() const + { + return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Latest)); + } + + Version Version::CreateLatest() + { + Version result; + result.m_parts.emplace_back(0, std::string{ s_Version_Part_Latest }); + return result; + } + + bool Version::IsUnknown() const + { + return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Unknown)); + } + + Version Version::CreateUnknown() + { + Version result; + result.m_parts.emplace_back(0, std::string{ s_Version_Part_Unknown }); + return result; + } + Version::Part::Part(const std::string& part) { const char* begin = part.c_str(); @@ -124,6 +180,9 @@ namespace AppInstaller::Utility } } + Version::Part::Part(uint64_t integer, std::string other) : + Integer(integer), Other(std::move(other)) {} + bool Version::Part::operator<(const Part& other) const { if (Integer < other.Integer) @@ -203,4 +262,15 @@ namespace AppInstaller::Utility // else m_version >= other.m_version return false; } + + bool VersionAndChannel::IsUpdatedBy(const VersionAndChannel& other) const + { + // Channel crossing should not happen here. + if (!Utility::CaseInsensitiveEquals(m_channel.ToString(), other.m_channel.ToString())) + { + return false; + } + + return m_version < other.m_version; + } } diff --git a/src/AppInstallerRepositoryCore/AggregatedSource.cpp b/src/AppInstallerRepositoryCore/AggregatedSource.cpp @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "AggregatedSource.h" - -namespace AppInstaller::Repository -{ - AggregatedSource::AggregatedSource(std::string identifier) : - m_identifier(identifier) - { - m_details.Name = "AggregatedSource"; - m_details.IsAggregated = true; - } - - const SourceDetails& AggregatedSource::GetDetails() const - { - return m_details; - } - - const std::string& AggregatedSource::GetIdentifier() const - { - return m_identifier; - } - - SearchResult AggregatedSource::Search(const SearchRequest& request) const - { - SearchResult result; - - for (auto& source : m_sources) - { - auto oneSourceResult = source->Search(request); - - for (auto& r : oneSourceResult.Matches) - { - r.SourceName = source->GetDetails().Name; - result.Matches.emplace_back(std::move(r)); - } - } - - SortResultMatches(result.Matches); - - if (request.MaximumResults > 0 && result.Matches.size() > request.MaximumResults) - { - result.Truncated = true; - result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); - } - - return result; - } - - void AggregatedSource::AddSource(std::shared_ptr<ISource> source) - { - m_sources.emplace_back(std::move(source)); - } - - void AggregatedSource::SortResultMatches(std::vector<ResultMatch>& matches) - { - struct ResultMatchComparator - { - // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. - bool operator() ( - const ResultMatch& match1, - const ResultMatch& match2) - { - if (match1.MatchCriteria.Type != match2.MatchCriteria.Type) - { - return match2.MatchCriteria.Type > match1.MatchCriteria.Type; - } - - if (match1.MatchCriteria.Field != match2.MatchCriteria.Field) - { - return match2.MatchCriteria.Field > match1.MatchCriteria.Field; - } - - return false; - } - }; - - std::stable_sort(matches.begin(), matches.end(), ResultMatchComparator()); - } -} - diff --git a/src/AppInstallerRepositoryCore/AggregatedSource.h b/src/AppInstallerRepositoryCore/AggregatedSource.h @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#pragma once -#include "AppInstallerRepositorySource.h" - -namespace AppInstaller::Repository -{ - struct AggregatedSource : public ISource - { - explicit AggregatedSource(std::string identifier); - - AggregatedSource(const AggregatedSource&) = delete; - AggregatedSource& operator=(const AggregatedSource&) = delete; - - AggregatedSource(AggregatedSource&&) = default; - AggregatedSource& operator=(AggregatedSource&&) = default; - - ~AggregatedSource() = default; - - // Get the source's details. - const SourceDetails& GetDetails() const override; - - // Gets the source's identifier; a unique identifier independent of the name - // that will not change between a remove/add or between additional adds. - // Must be suitable for filesystem names. - const std::string& GetIdentifier() const override; - - // Execute a search on the source. - SearchResult Search(const SearchRequest & request) const override; - - // Adds a source to be aggregated. - void AddSource(std::shared_ptr<ISource> source); - - private: - std::vector<std::shared_ptr<ISource>> m_sources; - SourceDetails m_details; - std::string m_identifier; - - // Sorts a vector of results. - static void SortResultMatches(std::vector<ResultMatch>& matches); - }; -} - - diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -172,8 +172,9 @@ </Link> </ItemDefinitionGroup> <ItemGroup> - <ClInclude Include="AggregatedSource.h" /> + <ClInclude Include="CompositeSource.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> + <ClInclude Include="Microsoft\PredefinedInstalledSourceFactory.h" /> <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\CommandsTable.h" /> @@ -206,7 +207,7 @@ <ClInclude Include="SQLiteWrapper.h" /> </ItemGroup> <ItemGroup> - <ClCompile Include="AggregatedSource.cpp" /> + <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="ICU\SQLiteICU.c"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader> @@ -217,6 +218,7 @@ <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> </ClCompile> + <ClCompile Include="Microsoft\PredefinedInstalledSourceFactory.cpp" /> <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface_1_0.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\ManifestTable.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -114,9 +114,6 @@ <ClInclude Include="ICU\SQLiteICU.h"> <Filter>ICU</Filter> </ClInclude> - <ClInclude Include="AggregatedSource.h"> - <Filter>Header Files</Filter> - </ClInclude> <ClInclude Include="Microsoft\Schema\1_1\Interface.h"> <Filter>Microsoft\Schema\1_1</Filter> </ClInclude> @@ -129,6 +126,12 @@ <ClInclude Include="Microsoft\Schema\1_1\SearchResultsTable.h"> <Filter>Microsoft\Schema\1_1</Filter> </ClInclude> + <ClInclude Include="Microsoft\PredefinedInstalledSourceFactory.h"> + <Filter>Microsoft</Filter> + </ClInclude> + <ClInclude Include="CompositeSource.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -176,9 +179,6 @@ <ClCompile Include="ICU\SQLiteICU.c"> <Filter>ICU</Filter> </ClCompile> - <ClCompile Include="AggregatedSource.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="Microsoft\Schema\1_0\Interface_1_0.cpp"> <Filter>Microsoft\Schema\1_0</Filter> </ClCompile> @@ -191,6 +191,12 @@ <ClCompile Include="Microsoft\Schema\1_1\SearchResultsTable_1_1.cpp"> <Filter>Microsoft\Schema\1_1</Filter> </ClCompile> + <ClCompile Include="Microsoft\PredefinedInstalledSourceFactory.cpp"> + <Filter>Microsoft</Filter> + </ClCompile> + <ClCompile Include="CompositeSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -0,0 +1,578 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "CompositeSource.h" + +namespace AppInstaller::Repository +{ + using namespace std::string_view_literals; + + namespace + { + Utility::VersionAndChannel GetVACFromVersion(IPackageVersion* packageVersion) + { + return { + Utility::Version(packageVersion->GetProperty(PackageVersionProperty::Version)), + Utility::Channel(packageVersion->GetProperty(PackageVersionProperty::Channel)) + }; + } + + // A composite package for the CompositeSource. + struct CompositePackage : public IPackage + { + CompositePackage(std::shared_ptr<IPackage> installedPackage, std::shared_ptr<IPackage> availablePackage = {}) : + m_installedPackage(std::move(installedPackage)), m_availablePackage(std::move(availablePackage)) + { + // Grab the installed version's channel to allow for filtering in calls to get available info. + if (m_installedPackage) + { + m_installedChannel = m_installedPackage->GetInstalledVersion()->GetProperty(PackageVersionProperty::Channel); + } + } + + Utility::LocIndString GetProperty(PackageProperty property) const override + { + std::shared_ptr<IPackageVersion> truth = GetLatestAvailableVersion(); + if (!truth) + { + truth = GetInstalledVersion(); + } + + switch (property) + { + case PackageProperty::Id: + return truth->GetProperty(PackageVersionProperty::Id); + case PackageProperty::Name: + return truth->GetProperty(PackageVersionProperty::Name); + default: + THROW_HR(E_UNEXPECTED); + } + } + + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + if (m_installedPackage) + { + return m_installedPackage->GetInstalledVersion(); + } + + return {}; + } + + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + if (m_availablePackage) + { + std::vector<PackageVersionKey> result = m_availablePackage->GetAvailableVersionKeys(); + std::string_view channel = m_installedChannel; + + // 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::CaseInsensitiveEquals(pvk.Channel, channel); }), + result.end()); + + return result; + } + + return {}; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + { + return GetAvailableVersion({ "", "", m_installedChannel.get() }); + } + + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + if (m_availablePackage) + { + return m_availablePackage->GetAvailableVersion(versionKey); + } + + return {}; + } + + bool IsUpdateAvailable() const override + { + auto installed = GetInstalledVersion(); + + if (!installed) + { + return false; + } + + auto latest = GetLatestAvailableVersion(); + + return (latest && (GetVACFromVersion(installed.get()).IsUpdatedBy(GetVACFromVersion(latest.get())))); + } + + void SetAvailablePackage(std::shared_ptr<IPackage> availablePackage) + { + m_availablePackage = std::move(availablePackage); + } + + private: + std::shared_ptr<IPackage> m_installedPackage; + Utility::LocIndString m_installedChannel; + std::shared_ptr<IPackage> m_availablePackage; + }; + + // A sentinel package with an unknown version. + struct UnknownAvailablePackage : public IPackage + { + static constexpr std::string_view Version = "Unknown"sv; + + struct UnknownAvailablePackageVersion : public IPackageVersion + { + Utility::LocIndString GetProperty(PackageVersionProperty property) const override + { + switch (property) + { + case AppInstaller::Repository::PackageVersionProperty::Version: + return Utility::LocIndString{ Version }; + default: + return {}; + } + } + + std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty) const override + { + return {}; + }; + + Manifest::Manifest GetManifest() const override + { + return {}; + } + + std::map<std::string, std::string> GetInstallationMetadata() const override + { + return {}; + } + }; + + Utility::LocIndString GetProperty(PackageProperty) const override + { + return {}; + } + + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + return {}; + } + + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + return { { {}, Version, {} } }; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + { + return std::make_shared<UnknownAvailablePackageVersion>(); + } + + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey&) const override + { + return std::make_shared<UnknownAvailablePackageVersion>(); + } + + bool IsUpdateAvailable() const override + { + // Lie here so that list and upgrade will carry on to be able to output the diagnositic information. + return true; + } + }; + + // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. + struct ResultMatchComparator + { + bool operator() ( + const ResultMatch& match1, + const ResultMatch& match2) + { + if (match1.MatchCriteria.Type != match2.MatchCriteria.Type) + { + return match1.MatchCriteria.Type < match2.MatchCriteria.Type; + } + + if (match1.MatchCriteria.Field != match2.MatchCriteria.Field) + { + return match1.MatchCriteria.Field < match2.MatchCriteria.Field; + } + + return false; + } + }; + + // Stores data to enable correlation between installed and available packages. + struct CompositeResult : public SearchResult + { + // A system reference string. + struct SystemReferenceString + { + SystemReferenceString(PackageMatchField field, Utility::LocIndString string) : + Field(field), String(string) {} + + bool operator<(const SystemReferenceString& other) const + { + if (Field < other.Field) + { + return true; + } + + return String < other.String; + } + + PackageMatchField Field; + Utility::LocIndString String; + }; + + // Data relevant to correlation for a package. + struct PackageData + { + std::vector<SystemReferenceString> SystemReferenceStrings; + }; + + // Data relevant to correlation for an installed package. + struct InstalledPackageData : public PackageData + { + size_t MatchIndex; + }; + + // For a given package version, prepares the results for it. + InstalledPackageData ReserveInstalledPackageSlot(IPackageVersion* installedVersion) + { + InstalledPackageData result; + result.MatchIndex = Matches.size(); + + HandleSystemReferenceStringTypeForReserveInstalledPackageSlot( + installedVersion, + PackageVersionMultiProperty::PackageFamilyName, + PackageMatchField::PackageFamilyName, + "package family name"sv, + result); + + HandleSystemReferenceStringTypeForReserveInstalledPackageSlot( + installedVersion, + PackageVersionMultiProperty::ProductCode, + PackageMatchField::ProductCode, + "product code"sv, + result); + + return result; + } + + // Check for a package already in the result that should have been correlated already. + // If we find one, see if we should upgrade it's match criteria. + // If we don't, return package data for further use. + std::optional<PackageData> CheckForExistingResultFromAvailablePackageMatch(const ResultMatch& match) + { + bool foundExistingPackage = false; + PackageData result; + + auto latestVersion = match.Package->GetLatestAvailableVersion(); + + foundExistingPackage = HandleSystemReferenceStringTypeForCheckForExistingResultFromAvailablePackageMatch( + match, + latestVersion.get(), + PackageVersionMultiProperty::PackageFamilyName, + PackageMatchField::PackageFamilyName, + "package family name"sv, + result); + + foundExistingPackage = HandleSystemReferenceStringTypeForCheckForExistingResultFromAvailablePackageMatch( + match, + latestVersion.get(), + PackageVersionMultiProperty::ProductCode, + PackageMatchField::ProductCode, + "product code"sv, + result) || foundExistingPackage; + + if (foundExistingPackage) + { + return {}; + } + else + { + return result; + } + } + + private: + void HandleSystemReferenceStringTypeForReserveInstalledPackageSlot( + IPackageVersion* installedVersion, + PackageVersionMultiProperty prop, + PackageMatchField field, + std::string_view logType, + InstalledPackageData& data) + { + for (auto&& string : installedVersion->GetMultiProperty(prop)) + { + SystemReferenceString srs(field, std::move(string)); + + if (m_systemReferenceMap.find(srs) != m_systemReferenceMap.end()) + { + AICLI_LOG(Repo, Warning, << "Multiple installed packages found with " << logType << " [" << srs.String << "], ignoring secondary packages for correlation."); + } + else + { + data.SystemReferenceStrings.emplace_back(srs); + m_systemReferenceMap.emplace(std::move(srs), data.MatchIndex); + } + } + } + + bool HandleSystemReferenceStringTypeForCheckForExistingResultFromAvailablePackageMatch( + const ResultMatch& match, + IPackageVersion* availableVersion, + PackageVersionMultiProperty prop, + PackageMatchField field, + std::string_view logType, + PackageData& data) + { + bool foundExistingPackage = false; + + for (auto&& string : availableVersion->GetMultiProperty(prop)) + { + SystemReferenceString srs(field, std::move(string)); + + auto itr = m_systemReferenceMap.find(srs); + if (itr != m_systemReferenceMap.end()) + { + foundExistingPackage = true; + + if (ResultMatchComparator{}(match, Matches[itr->second])) + { + AICLI_LOG(Repo, Verbose, << "Found existing result by " << logType << " [" << srs.String << "], increasing match criteria."); + Matches[itr->second].MatchCriteria = match.MatchCriteria; + } + } + + data.SystemReferenceStrings.emplace_back(std::move(srs)); + } + + return foundExistingPackage; + } + + // Maps for storing quick references to results based on their system reference string. + std::map<SystemReferenceString, size_t> m_systemReferenceMap; + }; + } + + CompositeSource::CompositeSource(std::string identifier) : + m_identifier(identifier) + { + m_details.Name = "CompositeSource"; + } + + const SourceDetails& CompositeSource::GetDetails() const + { + return m_details; + } + + const std::string& CompositeSource::GetIdentifier() const + { + return m_identifier; + } + + // The composite search needs to take several steps to get results, and due to the + // potential for different information spread across multiple sources, base searches + // need to be performed in both installed and available. + // + // If an installed source is present, then the searches should only return packages + // that are installed. This means that the base searches against available sources + // will only return results where a match is found in the installed source. + SearchResult CompositeSource::Search(const SearchRequest& request) const + { + if (m_installedSource) + { + return SearchInstalled(request); + } + else + { + return SearchAvailable(request); + } + } + + void CompositeSource::AddAvailableSource(std::shared_ptr<ISource> source) + { + m_availableSources.emplace_back(std::move(source)); + } + + void CompositeSource::SetInstalledSource(std::shared_ptr<ISource> source) + { + m_installedSource = std::move(source); + } + + // An installed search first finds all installed packages that match the request, then correlates with available sources. + // Next the search is performed against the available sources and correlated with the installed source. A result will only + // be added if there exists an installed package that was not found by the initial search. + // This allows for search terms to find installed packages by their available metadata, as well as the local values. + SearchResult CompositeSource::SearchInstalled(const SearchRequest& request) const + { + CompositeResult result; + + // Search installed source + SearchResult installedResult = m_installedSource->Search(request); + result.Truncated = installedResult.Truncated; + + for (auto&& match : installedResult.Matches) + { + auto compositePackage = std::make_shared<CompositePackage>(std::move(match.Package)); + + // Create a search request to run against all available sources + // TODO: Determine if we should create a single search or one for each installed package. + SearchRequest systemReferenceSearch; + + auto installedVersion = compositePackage->GetInstalledVersion(); + auto installedPackageData = result.ReserveInstalledPackageSlot(installedVersion.get()); + + if (!installedPackageData.SystemReferenceStrings.empty()) + { + for (const auto& srs : installedPackageData.SystemReferenceStrings) + { + systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String)); + } + + std::shared_ptr<IPackage> availablePackage; + + // Search sources and add to result + for (const auto& source : m_availableSources) + { + // See if a previous iteration found a package + if (availablePackage) + { + break; + } + + SearchResult availableResult = source->Search(systemReferenceSearch); + + if (availableResult.Matches.empty()) + { + continue; + } + + if (availableResult.Matches.size() == 1) + { + availablePackage = std::move(availableResult.Matches[0].Package); + break; + } + else // availableResult.Matches.size() > 1 + { + auto id = installedVersion->GetProperty(PackageVersionProperty::Id); + + AICLI_LOG(Repo, Info, + << "Found multiple matches for installed package [" << id << "] in source [" << source->GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); + + // More than one match found for the system reference; run some heuristics to check for a match + for (auto&& availableMatch : availableResult.Matches) + { + auto matchId = availableMatch.Package->GetLatestAvailableVersion()->GetProperty(PackageVersionProperty::Id); + + AICLI_LOG(Repo, Info, << " Checking system reference match with package id: " << matchId); + + if (Utility::CaseInsensitiveEquals(id, matchId)) + { + availablePackage = std::move(availableMatch.Package); + break; + } + } + + // We did not find an exact match on Id in the results + if (!availablePackage) + { + AICLI_LOG(Repo, Warning, << " Appropriate available package could not be determined, setting availablility state to unknown"); + availablePackage = std::make_shared<UnknownAvailablePackage>(); + } + } + } + + compositePackage->SetAvailablePackage(std::move(availablePackage)); + } + + // Move the installed result into the composite result + result.Matches.emplace_back(std::move(compositePackage), std::move(match.MatchCriteria)); + } + + // Optimization for the "everything installed" case, no need to allow for reverse correlations + if (request.IsForEverything()) + { + return result; + } + + // Search available sources + auto availableResult = SearchAvailable(request); + + for (auto&& match : availableResult.Matches) + { + // Check for a package already in the result that should have been correlated already. + auto packageData = result.CheckForExistingResultFromAvailablePackageMatch(match); + + // If no package was found that was already in the results, do a correlation lookup with the installed + // source to create a new composite package entry if we find any packages there. + if (packageData && !packageData->SystemReferenceStrings.empty()) + { + // Create a search request to run against the installed source + SearchRequest systemReferenceSearch; + for (const auto& srs : packageData->SystemReferenceStrings) + { + systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String)); + } + + SearchResult installedCrossRef = m_installedSource->Search(systemReferenceSearch); + + for (auto&& crossRef : installedCrossRef.Matches) + { + // Ensure that we don't pick up the same package from two available sources by recording it in the map. + auto installedVersion = crossRef.Package->GetInstalledVersion(); + auto installedPackageData = result.ReserveInstalledPackageSlot(installedVersion.get()); + + result.Matches.emplace_back(std::make_shared<CompositePackage>(std::move(crossRef.Package), std::move(match.Package)), match.MatchCriteria); + } + } + } + + SortResultMatches(result.Matches); + + if (request.MaximumResults > 0 && result.Matches.size() > request.MaximumResults) + { + result.Truncated = true; + result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); + } + + return result; + } + + // An available search goes through each source, searching individually and then sorting the full result set. + SearchResult CompositeSource::SearchAvailable(const SearchRequest& request) const + { + SearchResult result; + + // Search available sources + for (const auto& source : m_availableSources) + { + auto oneSourceResult = source->Search(request); + + // Move all matches into the single result + for (auto&& match : oneSourceResult.Matches) + { + result.Matches.emplace_back(std::move(match)); + } + } + + SortResultMatches(result.Matches); + + if (request.MaximumResults > 0 && result.Matches.size() > request.MaximumResults) + { + result.Truncated = true; + result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); + } + + return result; + } + + void CompositeSource::SortResultMatches(std::vector<ResultMatch>& matches) + { + std::stable_sort(matches.begin(), matches.end(), ResultMatchComparator()); + } +} diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "AppInstallerRepositorySource.h" + +namespace AppInstaller::Repository +{ + struct CompositeSource : public ISource + { + explicit CompositeSource(std::string identifier); + + CompositeSource(const CompositeSource&) = delete; + CompositeSource& operator=(const CompositeSource&) = delete; + + CompositeSource(CompositeSource&&) = default; + CompositeSource& operator=(CompositeSource&&) = default; + + ~CompositeSource() = default; + + // ISource + + // Get the source's details. + const SourceDetails& GetDetails() const override; + + // Gets the source's identifier; a unique identifier independent of the name + // that will not change between a remove/add or between additional adds. + // Must be suitable for filesystem names. + const std::string& GetIdentifier() const override; + + // Gets a value indicating whether this source is a composite of other sources, + // and thus the packages may come from disparate sources as well. + bool IsComposite() const override { return true; } + + // Execute a search on the source. + SearchResult Search(const SearchRequest& request) const override; + + // ~ISource + + // Adds an available source to be aggregated. + void AddAvailableSource(std::shared_ptr<ISource> source); + + // Sets the installed source to be composited. + void SetInstalledSource(std::shared_ptr<ISource> source); + + private: + // Performs a search when an installed source is present. + // Will only return packages that are installed. + SearchResult SearchInstalled(const SearchRequest& request) const; + + // Performs a search when no installed source is present. + SearchResult SearchAvailable(const SearchRequest& request) const; + + // Sorts a vector of results. + static void SortResultMatches(std::vector<ResultMatch>& matches); + + std::shared_ptr<ISource> m_installedSource; + std::vector<std::shared_ptr<ISource>> m_availableSources; + SourceDetails m_details; + std::string m_identifier; + }; +} + + diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h @@ -8,6 +8,8 @@ namespace AppInstaller::Repository::Microsoft { + using namespace std::string_view_literals; + // A source where the index is precomputed and stored on a server within an optional MSIX package. // In addition, the manifest files are also individually available on the server. // Arg :: Expected to be a fully qualified path to the root of the data. diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Microsoft/PredefinedInstalledSourceFactory.h" +#include "Microsoft/SQLiteIndex.h" +#include "Microsoft/SQLiteIndexSource.h" + +using namespace std::string_literals; +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Microsoft +{ + namespace + { + // Populates the index with the ARP entries from the given root. + void PopulateIndexFromARP(SQLiteIndex& index, HKEY rootKey) + { + UNREFERENCED_PARAMETER(index); + UNREFERENCED_PARAMETER(rootKey); + } + + // Populates the index with the entries from MSIX. + void PopulateIndexFromMSIX(SQLiteIndex& index) + { + using namespace winrt::Windows::ApplicationModel; + using namespace winrt::Windows::Management::Deployment; + + // TODO: Consider if Optional packages should also be enumerated + PackageManager packageManager; + auto packages = packageManager.FindPackagesForUserWithPackageTypes({}, PackageTypes::Main); + + // Reuse the same manifest object, as we will be setting the same values every time. + Manifest::Manifest manifest; + // Add one installer for storing the package family name. + manifest.Installers.emplace_back(); + // Every package will have the same tags currently. + manifest.Tags = { "msix" }; + + // Fields in the index but not populated: + // AppMoniker - Not sure what we would put. + // Channel - We don't know this information here. + // Commands - We could open the manifest and look for these eventually. + // Tags - Not sure what else we could put in here. + for (const auto& package : packages) + { + // System packages are part of the OS, and cannot be managed by the user. + // Filter them out as there is no point in showing them in a package manager. + auto signatureKind = package.SignatureKind(); + if (signatureKind == PackageSignatureKind::System) + { + continue; + } + + auto packageId = package.Id(); + Utility::NormalizedString familyName = Utility::ConvertToUTF8(packageId.FamilyName()); + + manifest.Id = familyName; + manifest.Name = Utility::ConvertToUTF8(package.DisplayName()); + + if (manifest.Name.empty()) + { + manifest.Name = Utility::ConvertToUTF8(packageId.Name()); + } + + std::ostringstream strstr; + auto packageVersion = packageId.Version(); + strstr << packageVersion.Major << '.' << packageVersion.Minor << '.' << packageVersion.Build << '.' << packageVersion.Revision; + + manifest.Version = strstr.str(); + + manifest.Installers[0].PackageFamilyName = familyName; + + // Use the family name as a unique key for the path + index.AddManifest(manifest, std::filesystem::path{ packageId.FamilyName().c_str() }); + } + } + + // The factory for the predefined installed source. + struct Factory : public ISourceFactory + { + std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback& progress) override final + { + // TODO: Maybe we do need to use it? + UNREFERENCED_PARAMETER(progress); + + THROW_HR_IF(E_INVALIDARG, details.Type != PredefinedInstalledSourceFactory::Type()); + + // Determine the filter + PredefinedInstalledSourceFactory::Filter filter = PredefinedInstalledSourceFactory::StringToFilter(details.Arg); + AICLI_LOG(Repo, Info, << "Creating PredefinedInstalledSource with filter [" << PredefinedInstalledSourceFactory::FilterToString(filter) << ']'); + + // Create an in memory index + SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, Schema::Version::Latest()); + + // Put installed packages into the index + if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_System) + { + PopulateIndexFromARP(index, HKEY_LOCAL_MACHINE); + } + + if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_User) + { + PopulateIndexFromARP(index, HKEY_CURRENT_USER); + } + + if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::MSIX) + { + PopulateIndexFromMSIX(index); + } + + return std::make_shared<SQLiteIndexSource>(details, "*PredefinedInstalledSource", std::move(index), Synchronization::CrossProcessReaderWriteLock{}, true); + } + + void Add(SourceDetails&, IProgressCallback&) override final + { + // Add should never be needed, as this is predefined. + THROW_HR(E_NOTIMPL); + } + + void Update(const SourceDetails&, IProgressCallback&) override final + { + // Update could be used later, but not for now. + THROW_HR(E_NOTIMPL); + } + + void Remove(const SourceDetails&, IProgressCallback&) override final + { + // Similar to add, remove should never be needed. + THROW_HR(E_NOTIMPL); + } + }; + } + + std::string_view PredefinedInstalledSourceFactory::FilterToString(Filter filter) + { + switch (filter) + { + case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::None: + return "None"sv; + case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_System: + return "ARP_System"sv; + case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_User: + return "ARP_User"sv; + case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::MSIX: + return "MSIX"sv; + default: + return "Unknown"sv; + } + } + + PredefinedInstalledSourceFactory::Filter PredefinedInstalledSourceFactory::StringToFilter(std::string_view filter) + { + if (filter == FilterToString(Filter::ARP_System)) + { + return Filter::ARP_System; + } + else if (filter == FilterToString(Filter::ARP_User)) + { + return Filter::ARP_User; + } + else if (filter == FilterToString(Filter::MSIX)) + { + return Filter::MSIX; + } + else + { + return Filter::None; + } + } + + std::unique_ptr<ISourceFactory> PredefinedInstalledSourceFactory::Create() + { + return std::make_unique<Factory>(); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" + +#include <string_view> + +namespace AppInstaller::Repository::Microsoft +{ + using namespace std::string_view_literals; + + // A source of installed packages on the local system. + // Arg :: A value indicating how the list is to be filtered. + // Data :: Not used. + struct PredefinedInstalledSourceFactory + { + // Get the type string for this source. + static constexpr std::string_view Type() + { + return "Microsoft.Predefined.Installed"sv; + } + + // The filtering level for the source. + enum class Filter + { + None, + ARP_System, + ARP_User, + MSIX, + }; + + // Converts a filter to its string. + static std::string_view FilterToString(Filter filter); + + // Converts a string to its filter value. + static Filter StringToFilter(std::string_view filter); + + // Creates a source factory for this type. + static std::unique_ptr<ISourceFactory> Create(); + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -229,6 +229,11 @@ namespace AppInstaller::Repository::Microsoft return m_interface->GetPropertyByManifestId(m_dbconn, manifestId, property); } + std::vector<std::string> SQLiteIndex::GetMultiPropertyByManifestId(IdType manifestId, PackageVersionMultiProperty property) const + { + return m_interface->GetMultiPropertyByManifestId(m_dbconn, manifestId, property); + } + std::optional<SQLiteIndex::IdType> SQLiteIndex::GetManifestIdByKey(IdType id, std::string_view version, std::string_view channel) const { return m_interface->GetManifestIdByKey(m_dbconn, id, version, channel); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -97,6 +97,9 @@ namespace AppInstaller::Repository::Microsoft // Gets the string for the given property and manifest id, if present. std::optional<std::string> GetPropertyByManifestId(IdType manifestId, PackageVersionProperty property) const; + // Gets the string values for the given property and manifest id, if present. + std::vector<std::string> GetMultiPropertyByManifestId(IdType manifestId, PackageVersionMultiProperty property) const; + // Gets the manifest id for the given { id, version, channel }, if present. // If version is empty, gets the value for the 'latest' version. std::optional<IdType> GetManifestIdByKey(IdType id, std::string_view version, std::string_view channel) const; diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -41,14 +41,29 @@ namespace AppInstaller::Repository::Microsoft { switch (property) { - case PackageVersionProperty::SourceId: + case PackageVersionProperty::SourceIdentifier: return LocIndString{ GetSource()->GetIdentifier() }; + case PackageVersionProperty::SourceName: + return LocIndString{ GetSource()->GetDetails().Name }; default: // Values coming from the index will always be localized/independent. return LocIndString{ GetSource()->GetIndex().GetPropertyByManifestId(m_manifestId, property).value() }; } } + std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty property) const override + { + std::vector<Utility::LocIndString> result; + + for (auto&& value : GetSource()->GetIndex().GetMultiPropertyByManifestId(m_manifestId, property)) + { + // Values coming from the index will always be localized/independent. + result.emplace_back(std::move(value)); + } + + return result; + } + Manifest::Manifest GetManifest() const override { std::shared_ptr<const SQLiteIndexSource> source = GetSource(); @@ -95,17 +110,63 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex::IdType m_manifestId; }; - // The IPackage impl for SQLiteIndexSource. - struct Package : public SourceReference, public IPackage + // The base for IPackage implementations here. + struct PackageBase : public SourceReference { - Package(const std::shared_ptr<const SQLiteIndexSource>& source, SQLiteIndex::IdType idId) : + PackageBase(const std::shared_ptr<const SQLiteIndexSource>& source, SQLiteIndex::IdType idId) : SourceReference(source), m_idId(idId) {} + Utility::LocIndString GetProperty(PackageProperty property) const + { + Utility::LocIndString result; + + std::shared_ptr<IPackageVersion> truth = GetLatestVersionInternal(); + if (truth) + { + switch (property) + { + case PackageProperty::Id: + return truth->GetProperty(PackageVersionProperty::Id); + case PackageProperty::Name: + return truth->GetProperty(PackageVersionProperty::Name); + default: + THROW_HR(E_UNEXPECTED); + } + } + + return result; + } + + protected: + std::shared_ptr<IPackageVersion> GetLatestVersionInternal() const + { + std::shared_ptr<const SQLiteIndexSource> source = GetSource(); + std::optional<SQLiteIndex::IdType> manifestId = source->GetIndex().GetManifestIdByKey(m_idId, {}, {}); + + if (manifestId) + { + return std::make_shared<PackageVersion>(source, manifestId.value()); + } + + return {}; + } + + SQLiteIndex::IdType m_idId; + }; + + // The IPackage impl for SQLiteIndexSource of Available packages. + struct AvailablePackage : public PackageBase, public IPackage + { + using PackageBase::PackageBase; + // Inherited via IPackage + Utility::LocIndString GetProperty(PackageProperty property) const override + { + return PackageBase::GetProperty(property); + } + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override { - // Although an index might be the backing store for installed packages, the installed package version - // will be selected by external business logic. return {}; } @@ -124,21 +185,19 @@ namespace AppInstaller::Repository::Microsoft std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override { - // Although we could potentially increase efficiency here, this should be fine. - std::vector<PackageVersionKey> versions = GetAvailableVersionKeys(); - - if (!versions.empty()) - { - return GetAvailableVersion(versions[0]); - } - - return {}; + return GetLatestVersionInternal(); } std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override { std::shared_ptr<const SQLiteIndexSource> source = GetSource(); - THROW_HR_IF(E_INVALIDARG, !versionKey.SourceId.empty() && versionKey.SourceId != source->GetIdentifier()); + + // Ensure that this key targets this (or any) source + if (!versionKey.SourceId.empty() && versionKey.SourceId != source->GetIdentifier()) + { + return {}; + } + std::optional<SQLiteIndex::IdType> manifestId = source->GetIndex().GetManifestIdByKey(m_idId, versionKey.Version, versionKey.Channel); if (manifestId) @@ -153,14 +212,48 @@ namespace AppInstaller::Repository::Microsoft { return false; } + }; - private: - SQLiteIndex::IdType m_idId; + // The IPackage impl for SQLiteIndexSource of Installed packages. + struct InstalledPackage : public PackageBase, public IPackage + { + using PackageBase::PackageBase; + + // Inherited via IPackage + Utility::LocIndString GetProperty(PackageProperty property) const override + { + return PackageBase::GetProperty(property); + } + + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + return GetLatestVersionInternal(); + } + + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + return {}; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + { + return {}; + } + + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey&) const override + { + return {}; + } + + bool IsUpdateAvailable() const override + { + return false; + } }; } - SQLiteIndexSource::SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock) : - m_details(details), m_identifier(std::move(identifier)), m_lock(std::move(lock)), m_index(std::move(index)) + SQLiteIndexSource::SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock, bool isInstalledSource) : + m_details(details), m_identifier(std::move(identifier)), m_lock(std::move(lock)), m_isInstalled(isInstalledSource), m_index(std::move(index)) { } @@ -182,7 +275,18 @@ namespace AppInstaller::Repository::Microsoft std::shared_ptr<const SQLiteIndexSource> sharedThis = shared_from_this(); for (auto& indexResult : indexResults.Matches) { - result.Matches.emplace_back(std::make_unique<Package>(sharedThis, indexResult.first), std::move(indexResult.second)); + std::unique_ptr<IPackage> package; + + if (m_isInstalled) + { + package = std::make_unique<InstalledPackage>(sharedThis, indexResult.first); + } + else + { + package = std::make_unique<AvailablePackage>(sharedThis, indexResult.first); + } + + result.Matches.emplace_back(std::move(package), std::move(indexResult.second)); } result.Truncated = indexResults.Truncated; return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h @@ -13,7 +13,7 @@ namespace AppInstaller::Repository::Microsoft // A source that holds a SQLiteIndex and lock. struct SQLiteIndexSource : public std::enable_shared_from_this<SQLiteIndexSource>, public ISource { - SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock = {}); + SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock = {}, bool isInstalledSource = false); SQLiteIndexSource(const SQLiteIndexSource&) = delete; SQLiteIndexSource& operator=(const SQLiteIndexSource&) = delete; @@ -41,6 +41,7 @@ namespace AppInstaller::Repository::Microsoft SourceDetails m_details; std::string m_identifier; Synchronization::CrossProcessReaderWriteLock m_lock; + bool m_isInstalled; SQLiteIndex m_index; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h @@ -23,6 +23,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; SearchResult Search(const SQLite::Connection& connection, const SearchRequest& request) const override; std::optional<std::string> GetPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const override; + std::vector<std::string> GetMultiPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMultiProperty property) const override; std::optional<SQLite::rowid_t> GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const override; std::vector<Utility::VersionAndChannel> GetVersionKeysById(const SQLite::Connection& connection, SQLite::rowid_t id) const override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp @@ -405,8 +405,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const { - // If an empty request, get everything - if (!request.Query && request.Inclusions.empty() && request.Filters.empty()) + if (request.IsForEverything()) { std::vector<SQLite::rowid_t> ids = IdTable::GetAllRowIds(connection, request.MaximumResults); @@ -512,6 +511,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } } + std::vector<std::string> Interface::GetMultiPropertyByManifestId(const SQLite::Connection&, SQLite::rowid_t, PackageVersionMultiProperty) const + { + return {}; + } + std::optional<SQLite::rowid_t> Interface::GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const { return StaticGetManifestIdByKey(connection, id, version, channel); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp @@ -145,6 +145,31 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 savepoint.Commit(); } + std::vector<std::string> OneToManyTableGetValuesByManifestId( + const SQLite::Connection& connection, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t manifestId) + { + using QCol = SQLite::Builder::QualifiedColumn; + + std::vector<std::string> result; + + SQLite::Builder::StatementBuilder builder; + builder.Select(QCol(tableName, valueName)). + From({ tableName, s_OneToManyTable_MapTable_Suffix }).As("map").Join(tableName). + On(QCol("map", valueName), QCol(tableName, SQLite::RowIDName)).Where(QCol("map", s_OneToManyTable_MapTable_ManifestName)).Equals(manifestId); + + SQLite::Statement statement = builder.Prepare(connection); + + while (statement.Step()) + { + result.emplace_back(statement.GetColumn<std::string>(0)); + } + + return result; + } + void OneToManyTableEnsureExistsAndInsert(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, const std::vector<Utility::NormalizedString>& values, SQLite::rowid_t manifestId) @@ -247,12 +272,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 savepoint.Commit(); } - void OneToManyTablePrepareForPackaging(SQLite::Connection& connection, std::string_view tableName, bool useNamedIndeces, bool preserveValuesIndex) + void OneToManyTablePrepareForPackaging(SQLite::Connection& connection, std::string_view tableName, bool useNamedIndeces, bool preserveManifestIndex, bool preserveValuesIndex) { - SQLite::Builder::StatementBuilder dropMapTableIndexBuilder; - dropMapTableIndexBuilder.DropIndex({ tableName, s_OneToManyTable_MapTable_Suffix, s_OneToManyTable_MapTable_IndexSuffix }); + if (!preserveManifestIndex) + { + SQLite::Builder::StatementBuilder dropMapTableIndexBuilder; + dropMapTableIndexBuilder.DropIndex({ tableName, s_OneToManyTable_MapTable_Suffix, s_OneToManyTable_MapTable_IndexSuffix }); - dropMapTableIndexBuilder.Execute(connection); + dropMapTableIndexBuilder.Execute(connection); + } OneToOneTablePrepareForPackaging(connection, tableName, useNamedIndeces, preserveValuesIndex); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.h @@ -20,6 +20,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Create the tables. void CreateOneToManyTable(SQLite::Connection& connection, bool useNamedIndeces, std::string_view tableName, std::string_view valueName); + // Gets all values associated with the given manifest id. + std::vector<std::string> OneToManyTableGetValuesByManifestId( + const SQLite::Connection& connection, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t manifestId); + // Ensures that the value exists and inserts mapping entries. void OneToManyTableEnsureExistsAndInsert(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, @@ -34,7 +41,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void OneToManyTableDeleteIfNotNeededByManifestId(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, SQLite::rowid_t manifestId); // Removes data that is no longer needed for an index that is to be published. - void OneToManyTablePrepareForPackaging(SQLite::Connection& connection, std::string_view tableName, bool useNamedIndeces, bool preserveValuesIndex); + void OneToManyTablePrepareForPackaging(SQLite::Connection& connection, std::string_view tableName, bool useNamedIndeces, bool preserveManifestIndex, bool preserveValuesIndex); // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. @@ -78,6 +85,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 details::CreateOneToManyTable(connection, false, TableInfo::TableName(), TableInfo::ValueName()); } + // Gets all values associated with the given manifest id. + static std::vector<std::string> GetValuesByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId) + { + return details::OneToManyTableGetValuesByManifestId(connection, TableInfo::TableName(), TableInfo::ValueName(), manifestId); + } + // Ensures that all values exist in the data table, and inserts into the mapping table for the given manifest id. static void EnsureExistsAndInsert(SQLite::Connection& connection, const std::vector<Utility::NormalizedString>& values, SQLite::rowid_t manifestId) { @@ -97,15 +110,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // Removes data that is no longer needed for an index that is to be published. - static void PrepareForPackaging(SQLite::Connection& connection, bool preserveValuesIndex = false) + static void PrepareForPackaging(SQLite::Connection& connection, bool preserveManifestIndex, bool preserveValuesIndex = false) { - details::OneToManyTablePrepareForPackaging(connection, TableInfo::TableName(), true, preserveValuesIndex); + details::OneToManyTablePrepareForPackaging(connection, TableInfo::TableName(), true, preserveManifestIndex, preserveValuesIndex); } // Removes data that is no longer needed for an index that is to be published. static void PrepareForPackaging_deprecated(SQLite::Connection& connection) { - details::OneToManyTablePrepareForPackaging(connection, TableInfo::TableName(), false, false); + details::OneToManyTablePrepareForPackaging(connection, TableInfo::TableName(), false, false, false); } // Checks the consistency of the index to ensure that every referenced row exists. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h @@ -19,6 +19,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 void PrepareForPackaging(SQLite::Connection& connection) override; bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; SearchResult Search(const SQLite::Connection& connection, const SearchRequest& request) const override; + std::vector<std::string> GetMultiPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMultiProperty property) const override; protected: std::unique_ptr<V1_0::SearchResultsTable> CreateSearchResultsTable(const SQLite::Connection& connection) const override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp @@ -159,10 +159,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 V1_0::PathPartTable::ValueName(), }); - V1_0::TagsTable::PrepareForPackaging(connection); - V1_0::CommandsTable::PrepareForPackaging(connection); - PackageFamilyNameTable::PrepareForPackaging(connection, true); - ProductCodeTable::PrepareForPackaging(connection, true); + V1_0::TagsTable::PrepareForPackaging(connection, false); + V1_0::CommandsTable::PrepareForPackaging(connection, false); + PackageFamilyNameTable::PrepareForPackaging(connection, true, true); + ProductCodeTable::PrepareForPackaging(connection, true, true); savepoint.Commit(); @@ -218,6 +218,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 return V1_0::Interface::Search(connection, foldedRequest); } + std::vector<std::string> Interface::GetMultiPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMultiProperty property) const + { + switch (property) + { + case PackageVersionMultiProperty::PackageFamilyName: + return PackageFamilyNameTable::GetValuesByManifestId(connection, manifestId); + case PackageVersionMultiProperty::ProductCode: + return ProductCodeTable::GetValuesByManifestId(connection, manifestId); + default: + return V1_0::Interface::GetMultiPropertyByManifestId(connection, manifestId, property); + } + } + std::unique_ptr<V1_0::SearchResultsTable> Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const { return std::make_unique<SearchResultsTable>(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -60,6 +60,9 @@ namespace AppInstaller::Repository::Microsoft::Schema // Gets the string for the given property and manifest id, if present. virtual std::optional<std::string> GetPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const = 0; + // Gets the string values for the given property and manifest id, if present. + virtual std::vector<std::string> GetMultiPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMultiProperty property) const = 0; + // Gets the manifest id for the given { id, version, channel }, if present. // If version is empty, gets the value for the 'latest' version. virtual std::optional<SQLite::rowid_t> GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const = 0; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -81,21 +81,32 @@ namespace AppInstaller::Repository // The default of 0 will place no limit. size_t MaximumResults{}; + // Returns a value indicating whether this request is for all available data. + bool IsForEverything() const; + // Returns a string summarizing the search request. std::string ToString() const; }; - // A property of a package. + // A property of a package version. enum class PackageVersionProperty { Id, Name, - SourceId, + SourceIdentifier, + SourceName, Version, Channel, RelativePath, }; + // A property of a package version that can have multiple values. + enum class PackageVersionMultiProperty + { + PackageFamilyName, + ProductCode, + }; + // A single package version. struct IPackageVersion { @@ -104,6 +115,9 @@ namespace AppInstaller::Repository // Gets a property of this package version. virtual Utility::LocIndString GetProperty(PackageVersionProperty property) const = 0; + // Gets a property of this package version that can have multiple values. + virtual std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty property) const = 0; + // Gets the manifest of this package version. virtual Manifest::Manifest GetManifest() const = 0; @@ -121,11 +135,13 @@ namespace AppInstaller::Repository // A key to identify a package version within a package. struct PackageVersionKey { + PackageVersionKey() = default; + PackageVersionKey(Utility::NormalizedString sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : SourceId(std::move(sourceId)), Version(std::move(version)), Channel(std::move(channel)) {} // The source id that this version came from. - Utility::NormalizedString SourceId; + std::string SourceId; // The version. Utility::NormalizedString Version; @@ -134,11 +150,21 @@ namespace AppInstaller::Repository Utility::NormalizedString Channel; }; + // A property of a package. + enum class PackageProperty + { + Id, + Name, + }; + // A package, potentially containing information about it's local state and the available versions. struct IPackage { virtual ~IPackage() = default; + // Gets a property of this package. + virtual Utility::LocIndString GetProperty(PackageProperty property) const = 0; + // Gets the installed package information. virtual std::shared_ptr<IPackageVersion> GetInstalledVersion() const = 0; @@ -161,15 +187,12 @@ namespace AppInstaller::Repository struct ResultMatch { // The package found by the search request. - std::unique_ptr<IPackage> Package; + std::shared_ptr<IPackage> Package; // The highest order field on which the package matched the search. PackageMatchFilter MatchCriteria; - // The name of the source where the result is from. Used in aggregated source scenario. - std::string SourceName = {}; - - ResultMatch(std::unique_ptr<IPackage>&& p, PackageMatchFilter f) : Package(std::move(p)), MatchCriteria(std::move(f)) {} + ResultMatch(std::shared_ptr<IPackage> p, PackageMatchFilter f) : Package(std::move(p)), MatchCriteria(std::move(f)) {} }; // Search result data. diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -5,6 +5,7 @@ #include <AppInstallerProgress.h> #include <chrono> +#include <filesystem> #include <memory> #include <optional> #include <string> @@ -19,6 +20,7 @@ namespace AppInstaller::Repository { Default, User, + Predefined, }; std::string_view ToString(SourceOrigin origin); @@ -43,9 +45,6 @@ namespace AppInstaller::Repository // The origin of the source. SourceOrigin Origin = SourceOrigin::Default; - - // If the source is an aggregated source - bool IsAggregated = false; }; // Interface for interacting with a source from outside of the repository lib. @@ -62,6 +61,10 @@ namespace AppInstaller::Repository // in which case the identifier should begin with a '*' character. virtual const std::string& GetIdentifier() const = 0; + // Gets a value indicating whether this source is a composite of other sources, + // and thus the packages may come from disparate sources as well. + virtual bool IsComposite() const { return false; } + // Execute a search on the source. virtual SearchResult Search(const SearchRequest& request) const = 0; }; @@ -102,9 +105,8 @@ namespace AppInstaller::Repository // These sources are not under the direct control of the user, such as packages installed on the system. std::shared_ptr<ISource> OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress); - // Creates a composite source from input sources. - // The composite source will correlate entries from input sources. - std::shared_ptr<ISource> CreateCompositeSource(std::shared_ptr<ISource>& source1, std::shared_ptr<ISource>& source2); + // Creates a source that merges the installed packages with the given available packages. + std::shared_ptr<ISource> CreateCompositeSource(const std::shared_ptr<ISource>& installedSource, const std::shared_ptr<ISource>& availableSource); // Updates an existing source. // Return value indicates whether the named source was found. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -3,8 +3,9 @@ #include "pch.h" #include "Public/AppInstallerRepositorySource.h" -#include "AggregatedSource.h" +#include "CompositeSource.h" #include "SourceFactory.h" +#include "Microsoft/PredefinedInstalledSourceFactory.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" namespace AppInstaller::Repository @@ -368,6 +369,11 @@ namespace AppInstaller::Repository { return Microsoft::PreIndexedPackageSourceFactory::Create(); } + // Should always come from code, so no need for case insensitivity + else if (Microsoft::PredefinedInstalledSourceFactory::Type() == type) + { + return Microsoft::PredefinedInstalledSourceFactory::Create(); + } THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); } @@ -528,7 +534,7 @@ namespace AppInstaller::Repository else { AICLI_LOG(Repo, Info, << "Default source requested, multiple sources available, creating aggregated source."); - auto aggregatedSource = std::make_shared<AggregatedSource>("*DefaultSource"); + auto aggregatedSource = std::make_shared<CompositeSource>("*DefaultSource"); bool sourceUpdated = false; for (auto& source : currentSources) @@ -542,7 +548,7 @@ namespace AppInstaller::Repository UpdateSourceFromDetails(source, progress); sourceUpdated = true; } - aggregatedSource->AddSource(CreateSourceFromDetails(source, progress)); + aggregatedSource->AddAvailableSource(CreateSourceFromDetails(source, progress)); } if (sourceUpdated) @@ -578,36 +584,44 @@ namespace AppInstaller::Repository std::shared_ptr<ISource> OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress) { SourceDetails details; + details.Origin = SourceOrigin::Predefined; switch (source) { case PredefinedSource::Installed: - // TODO: Pull directly from factory - details.Type = "Microsoft.Predefined.Installed"; + details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); + details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::None); return CreateSourceFromDetails(details, progress); case PredefinedSource::ARP_System: - // TODO: Pull directly from factory - details.Type = "Microsoft.Predefined.ARP"; - details.Arg = "system"; + details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); + details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_System); return CreateSourceFromDetails(details, progress); case PredefinedSource::ARP_User: - // TODO: Pull directly from factory - details.Type = "Microsoft.Predefined.ARP"; - details.Arg = "user"; + details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); + details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_User); return CreateSourceFromDetails(details, progress); case PredefinedSource::MSIX: - // TODO: Pull directly from factory - details.Type = "Microsoft.Predefined.MSIX"; + details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); + details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::MSIX); return CreateSourceFromDetails(details, progress); } THROW_HR(E_UNEXPECTED); } - std::shared_ptr<ISource> CreateCompositeSource(std::shared_ptr<ISource>& source1, std::shared_ptr<ISource>&) + std::shared_ptr<ISource> CreateCompositeSource(const std::shared_ptr<ISource>& installedSource, const std::shared_ptr<ISource>& availableSource) { - // TODO: needs implementation - return source1; + std::shared_ptr<CompositeSource> result = std::dynamic_pointer_cast<CompositeSource>(availableSource); + + if (!result) + { + result = std::make_shared<CompositeSource>("*CompositeSource"); + result->AddAvailableSource(availableSource); + } + + result->SetInstalledSource(installedSource); + + return result; } bool UpdateSource(std::string_view name, IProgressCallback& progress) @@ -721,6 +735,11 @@ namespace AppInstaller::Repository } } + bool SearchRequest::IsForEverything() const + { + return (!Query.has_value() && Inclusions.empty() && Filters.empty()); + } + std::string SearchRequest::ToString() const { std::ostringstream result; diff --git a/src/AppInstallerRepositoryCore/SQLiteWrapper.cpp b/src/AppInstallerRepositoryCore/SQLiteWrapper.cpp @@ -254,7 +254,7 @@ namespace AppInstaller::Repository::SQLite m_rollbackTo = Statement::Create(connection, "ROLLBACK TO ["s + m_name + "]"); m_release = Statement::Create(connection, "RELEASE ["s + m_name + "]"); - AICLI_LOG(SQL, Info, << "Begin savepoint: " << m_name); + AICLI_LOG(SQL, Verbose, << "Begin savepoint: " << m_name); begin.Step(); } @@ -272,7 +272,7 @@ namespace AppInstaller::Repository::SQLite { if (m_inProgress) { - AICLI_LOG(SQL, Info, << "Roll back savepoint: " << m_name); + AICLI_LOG(SQL, Verbose, << "Roll back savepoint: " << m_name); m_rollbackTo.Step(true); // 'ROLLBACK TO' *DOES NOT* remove the savepoint from the transaction stack. // In order to remove it, we must RELEASE. Since we just invoked a ROLLBACK TO @@ -286,7 +286,7 @@ namespace AppInstaller::Repository::SQLite { if (m_inProgress) { - AICLI_LOG(SQL, Info, << "Commit savepoint: " << m_name); + AICLI_LOG(SQL, Verbose, << "Commit savepoint: " << m_name); m_release.Step(true); m_inProgress = false; } diff --git a/src/AppInstallerRepositoryCore/SQLiteWrapper.h b/src/AppInstallerRepositoryCore/SQLiteWrapper.h @@ -14,6 +14,8 @@ #include <type_traits> #include <utility> +#define SQLITE_MEMORY_DB_CONNECTION_TARGET ":memory:" + namespace AppInstaller::Repository::SQLite { // The name of the rowid column in SQLite. diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -30,6 +30,7 @@ #include <winrt/Windows.ApplicationModel.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Management.Deployment.h> #include <winrt/Windows.Storage.h> #include <algorithm>