diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 9895208eca..9d09d35b32 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,3 +1,31 @@ ## New in v1.29 +# New Feature: Source Priority + +> [!NOTE] +> Experimental under `sourcePriority`; defaulted to disabled. + +With this feature, one can assign a numerical priority to sources when added or later through the `source edit` +command. Sources with higher priority are sorted first in the list of sources, which results in them getting put first +in the results if other things are equal. + +> [!TIP] +> Search result ordering in winget is currently based on these values in this order: +> 1. Match quality (how well a valid field matches the search request) +> 2. Match field (which field was matched against the search request) +> 3. Source order (was always relevant, but with priority you can more easily affect this) + +Beyond the ability to slightly affect the result ordering, commands that primarily target available packages +(largely `install`) will now prefer to use a single result from a source with higher priority rather than prompting for +disambiguation from the user. Said another way, if multiple sources return results but only one of those sources has +the highest priority value (and it returned only one result) then that package will be used rather than giving a +"multiple packages were found" error. This has been applied to both winget CLI and PowerShell module commands. + +### REST result match criteria update + +Along with the source priority change, the results from REST sources (like `msstore`) now attempt to correctly set the +match criteria that factor into the result ordering. This will prevent them from being sorted to the top automatically. + +## Bug Fixes + diff --git a/doc/Settings.md b/doc/Settings.md index 5eb1ff765e..d33a8b4271 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -393,3 +393,15 @@ This feature enables support for fonts via `winget settings`. The `winget font l "fonts": true }, ``` + +### sourcePriority + +This feature enables sources to have a priority value assigned. Sources with a higher priority will appear earlier in search results and will be selected for installing new packages when multiple sources have a matching package. + +Note that search result ordering is dependent on several factors, and source priority is the lowest field in that currently (match quality and field are more important). + +```json + "experimentalFeatures": { + "sourcePriority": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index daa289b75c..4cc0c632d3 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -328,6 +328,11 @@ "description": "Enable support for managing fonts", "type": "boolean", "default": false + }, + "sourcePriority": { + "description": "Enable source priority feature", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 2622ed7163..ab0f2f6c23 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -229,6 +229,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F49C EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ComInprocTestbed", "ComInprocTestbed\ComInprocTestbed.vcxproj", "{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DSC", "DSC", "{40D7CA7F-EB86-4345-9641-AD27180C559D}" + ProjectSection(SolutionItems) = preProject + PowerShell\Microsoft.WinGet.DSC\Microsoft.WinGet.DSC.psd1 = PowerShell\Microsoft.WinGet.DSC\Microsoft.WinGet.DSC.psd1 + PowerShell\Microsoft.WinGet.DSC\Microsoft.WinGet.DSC.psm1 = PowerShell\Microsoft.WinGet.DSC\Microsoft.WinGet.DSC.psm1 + EndProjectSection +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WinGet.UnitTests", "PowerShell\Microsoft.WinGet.UnitTests\Microsoft.WinGet.UnitTests.csproj", "{5421394F-5619-4E4B-8923-F3FB30D5EFAD}" EndProject Global @@ -1100,6 +1106,7 @@ Global {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {F49C4C89-447E-4D15-B38B-5A8DCFB134AF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {40D7CA7F-EB86-4345-9641-AD27180C559D} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {5421394F-5619-4E4B-8923-F3FB30D5EFAD} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index acaaf0c022..6703130fd2 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -123,6 +123,8 @@ namespace AppInstaller::CLI return { type, "trust-level"_liv }; case Execution::Args::Type::SourceEditExplicit: return { type, "explicit"_liv, 'e' }; + case Execution::Args::Type::SourcePriority: + return { type, "priority"_liv, 'p' }; // Hash Command case Execution::Args::Type::HashFile: @@ -415,7 +417,9 @@ namespace AppInstaller::CLI case Args::Type::SourceExplicit: return Argument{ type, Resource::String::SourceExplicitArgumentDescription, ArgumentType::Flag }; case Args::Type::SourceEditExplicit: - return Argument{ type, Resource::String::SourceEditExplicitArgumentDescription, ArgumentType::Positional }; + return Argument{ type, Resource::String::SourceEditExplicitArgumentDescription, ArgumentType::Standard }; + case Args::Type::SourcePriority: + return Argument{ type, Resource::String::SourcePriorityArgumentDescription, ArgumentType::Standard, ExperimentalFeature::Feature::SourcePriority }; case Args::Type::SourceTrustLevel: return Argument{ type, Resource::String::SourceTrustLevelArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::ValidateManifest: diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h index 45b55dc011..8e18a745d5 100644 --- a/src/AppInstallerCLICore/Commands/DscComposableObject.h +++ b/src/AppInstallerCLICore/Commands/DscComposableObject.h @@ -82,6 +82,20 @@ namespace AppInstaller::CLI } }; + template <> + struct GetJsonTypeValue + { + static int32_t Get(const Json::Value& value) + { + return value.asInt(); + } + + static Json::ValueType SchemaType() + { + return Json::ValueType::intValue; + } + }; + template <> struct GetJsonTypeValue { diff --git a/src/AppInstallerCLICore/Commands/DscSourceResource.cpp b/src/AppInstallerCLICore/Commands/DscSourceResource.cpp index 0623db11b2..461c2c0efd 100644 --- a/src/AppInstallerCLICore/Commands/DscSourceResource.cpp +++ b/src/AppInstallerCLICore/Commands/DscSourceResource.cpp @@ -6,6 +6,7 @@ #include "Resources.h" #include "Workflows/SourceFlow.h" #include +#include using namespace AppInstaller::Utility::literals; using namespace AppInstaller::Repository; @@ -20,8 +21,9 @@ namespace AppInstaller::CLI WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(TrustLevelProperty, std::string, TrustLevel, "trustLevel", Resource::String::DscResourcePropertyDescriptionSourceTrustLevel, ({ "undefined", "none", "trusted" }), "undefined"); WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ExplicitProperty, bool, Explicit, "explicit", Resource::String::DscResourcePropertyDescriptionSourceExplicit); WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionAcceptAgreements); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(PriorityProperty, int32_t, Priority, "priority", Resource::String::DscResourcePropertyDescriptionSourcePriority); - using SourceResourceObject = DscComposableObject; + using SourceResourceObject = DscComposableObject; std::string TrustLevelStringFromFlags(SourceTrustLevel trustLevel) { @@ -109,6 +111,11 @@ namespace AppInstaller::CLI Output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel)); Output.Explicit(source.Explicit); + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority)) + { + Output.Priority(source.Priority); + } + std::vector sources; sources.emplace_back(source); SubContext->Add(std::move(sources)); @@ -148,6 +155,14 @@ namespace AppInstaller::CLI SubContext->Args.AddArg(Execution::Args::Type::SourceExplicit); } + std::string priorityString; + if (Input.Priority()) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority)); + priorityString = std::to_string(Input.Priority().value()); + SubContext->Args.AddArg(Execution::Args::Type::SourcePriority, priorityString); + } + *SubContext << Workflow::EnsureRunningAsAdmin << Workflow::CreateSourceForSourceAdd << @@ -168,11 +183,50 @@ namespace AppInstaller::CLI Workflow::RemoveSources; } + void Edit() + { + AICLI_LOG(CLI, Verbose, << "Source::Edit invoked"); + + if (!SubContext->Args.Contains(Execution::Args::Type::SourceName)) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceName, Input.SourceName().value()); + } + + std::string explicitString; + if (Input.Explicit()) + { + explicitString = Utility::ConvertBoolToString(Input.Explicit().value()); + SubContext->Args.AddArg(Execution::Args::Type::SourceEditExplicit, explicitString); + } + + std::string priorityString; + if (Input.Priority()) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority)); + priorityString = std::to_string(Input.Priority().value()); + SubContext->Args.AddArg(Execution::Args::Type::SourcePriority, priorityString); + } + + *SubContext << + Workflow::EnsureRunningAsAdmin << + Workflow::EditSources; + } + void Replace() { AICLI_LOG(CLI, Verbose, << "Source::Replace invoked"); - Remove(); - Add(); + + // Check to see if we can use an edit rather than a complete replacement + if (TestArgument() && TestType() && TestTrustLevel()) + { + // Implies that the failing portion of Test was in the editable Explicit or Priority properties + Edit(); + } + else + { + Remove(); + Add(); + } } // Determines if the current Output values match the Input values state. @@ -185,8 +239,9 @@ namespace AppInstaller::CLI { if (Output.Exist().value()) { - AICLI_LOG(CLI, Verbose, << "Source::Test needed to inspect these properties: Argument(" << TestArgument() << "), Type(" << TestType() << "), TrustLevel(" << TestTrustLevel() << "), Explicit(" << TestExplicit() << ")"); - return TestArgument() && TestType() && TestTrustLevel() && TestExplicit(); + AICLI_LOG(CLI, Verbose, << "Source::Test needed to inspect these properties: Argument(" << TestArgument() << + "), Type(" << TestType() << "), TrustLevel(" << TestTrustLevel() << "), Explicit(" << TestExplicit() << "), Priority(" << TestPriority() << ")"); + return TestArgument() && TestType() && TestTrustLevel() && TestExplicit() && TestPriority(); } else { @@ -233,6 +288,11 @@ namespace AppInstaller::CLI { result.append(std::string{ ExplicitProperty::Name() }); } + + if (!TestPriority()) + { + result.append(std::string{ PriorityProperty::Name() }); + } } return result; @@ -308,6 +368,26 @@ namespace AppInstaller::CLI return true; } } + + bool TestPriority() + { + if (Input.Priority()) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority)); + if (Output.Priority()) + { + return Input.Priority().value() == Output.Priority().value(); + } + else + { + return false; + } + } + else + { + return true; + } + } }; } @@ -418,6 +498,12 @@ namespace AppInstaller::CLI output.Type(source.Type); output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel)); output.Explicit(source.Explicit); + + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourcePriority)) + { + output.Priority(source.Priority); + } + WriteJsonOutputLine(context, output.ToJson()); } } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp index bf7a2b1a12..f98ff24910 100644 --- a/src/AppInstallerCLICore/Commands/SourceCommand.cpp +++ b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -12,6 +12,22 @@ namespace AppInstaller::CLI using namespace AppInstaller::CLI::Execution; using namespace std::string_view_literals; + namespace + { + void ValidateSourcePriorityArgument(const Args& execArgs) + { + if (execArgs.Contains(Execution::Args::Type::SourcePriority)) + { + std::string_view priorityArg = execArgs.GetArg(Execution::Args::Type::SourcePriority); + auto convertedArg = Utility::TryConvertStringToInt32(priorityArg); + if (!convertedArg.has_value()) + { + throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues(Argument::ForType(Execution::Args::Type::SourcePriority).Name())); + } + } + } + } + Utility::LocIndView s_SourceCommand_HelpLink = "https://aka.ms/winget-command-source"_liv; std::vector> SourceCommand::GetCommands() const @@ -57,6 +73,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::SourceExplicit), + Argument::ForType(Args::Type::SourcePriority), }; } @@ -96,6 +113,8 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::SourceTrustLevel).Name, Utility::Join(","_liv, validOptions))); } } + + ValidateSourcePriorityArgument(execArgs); } void SourceAddCommand::ExecuteInternal(Context& context) const @@ -321,6 +340,7 @@ namespace AppInstaller::CLI return { Argument::ForType(Args::Type::SourceName).SetRequired(true), Argument::ForType(Args::Type::SourceEditExplicit), + Argument::ForType(Args::Type::SourcePriority), }; } @@ -354,6 +374,8 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::SourceEditExplicit).Name(), validOptions)); } } + + ValidateSourcePriorityArgument(execArgs); } void SourceEditCommand::ExecuteInternal(Context& context) const diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 02322efd9c..895b76b2ef 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -65,6 +65,7 @@ namespace AppInstaller::CLI::Execution ForceSourceReset, SourceExplicit, SourceTrustLevel, + SourcePriority, SourceEditExplicit, //Hash Command diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index bc80a4e962..4afcc159be 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -250,6 +250,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceType); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceExplicit); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourcePriority); WINGET_DEFINE_RESOURCE_STRINGID(DscSourceResourceShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscSourceResourceLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); @@ -477,6 +478,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MultipleExclusiveArgumentsProvided); WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); + WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFoundFilteredBySourcePriority); WINGET_DEFINE_RESOURCE_STRINGID(MultipleUnsupportedNestedInstallersSpecified); WINGET_DEFINE_RESOURCE_STRINGID(MultiQueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MultiQueryPackageAlreadyInstalled); @@ -722,6 +724,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListName); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoneFound); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoSources); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListPriority); WINGET_DEFINE_RESOURCE_STRINGID(SourceListTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(SourceListType); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdated); @@ -731,6 +734,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenFailedSuggestion); WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenPredefinedFailedSuggestion); WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenWithFailedUpdate); + WINGET_DEFINE_RESOURCE_STRINGID(SourcePriorityArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 17d6fcfd99..224a3a31bf 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -64,6 +64,9 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_Setting_WinGetSource_Name = L"name"; constexpr std::wstring_view s_Setting_WinGetSource_Arg = L"argument"; constexpr std::wstring_view s_Setting_WinGetSource_Type = L"type"; + constexpr std::wstring_view s_Setting_WinGetSource_TrustLevel = L"trustLevel"; + constexpr std::wstring_view s_Setting_WinGetSource_Explicit = L"explicit"; + constexpr std::wstring_view s_Setting_WinGetSource_Priority = L"priority"; constexpr std::wstring_view s_Predefined_PowerShell_PackageId = L"Microsoft.PowerShell"; constexpr std::wstring_view s_Predefined_PowerShell_PackageSource = L"winget"; @@ -1378,6 +1381,18 @@ namespace AppInstaller::CLI::Workflow settings.Insert(s_Setting_WinGetSource_Name, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Name))); settings.Insert(s_Setting_WinGetSource_Arg, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Arg))); settings.Insert(s_Setting_WinGetSource_Type, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Type))); + if (WI_IsFlagSet(source.Details.TrustLevel, Repository::SourceTrustLevel::Trusted)) + { + settings.Insert(s_Setting_WinGetSource_TrustLevel, PropertyValue::CreateString(L"trusted")); + } + if (source.Details.Explicit) + { + settings.Insert(s_Setting_WinGetSource_Explicit, PropertyValue::CreateBoolean(true)); + } + if (source.Details.Priority != 0) + { + settings.Insert(s_Setting_WinGetSource_Priority, PropertyValue::CreateInt32(source.Details.Priority)); + } unit.Settings(settings); unit.Environment().Context(SecurityContext::Elevated); @@ -1407,7 +1422,7 @@ namespace AppInstaller::CLI::Workflow } } - ConfigurationUnit CreateWinGetPackageUnit(const PackageCollection::Package& package, const PackageCollection::Source& source, bool includeVersion, const std::optional& dependentUnit, std::wstring_view unitType) + ConfigurationUnit CreateWinGetPackageUnit(const PackageCollection::Package& package, const PackageCollection::Source& source, bool includeVersion, const ConfigurationUnit& dependentUnit, std::wstring_view unitType) { std::wstring packageIdWide = Utility::ConvertToUTF16(package.Id); std::wstring sourceNameWide = Utility::ConvertToUTF16(source.Details.Name); @@ -1435,10 +1450,10 @@ namespace AppInstaller::CLI::Workflow // TODO: We may consider setting security environment based on installer elevation requirements? // Add dependency if needed. - if (dependentUnit.has_value()) + if (dependentUnit) { auto dependencies = winrt::single_threaded_vector(); - dependencies.Append(dependentUnit.value().Identifier()); + dependencies.Append(dependentUnit.Identifier()); unit.Dependencies(std::move(dependencies)); } @@ -1806,13 +1821,9 @@ namespace AppInstaller::CLI::Workflow for (const auto& source : context.Get().Sources) { - // Create WinGetSource unit for non well known source. - std::optional sourceUnit; - if (!CheckForWellKnownSource(source.Details)) - { - sourceUnit = anon::CreateWinGetSourceUnit(source, sourceUnitType); - configContext.Set().Units().Append(sourceUnit.value()); - } + // Create WinGetSource unit + ConfigurationUnit sourceUnit = anon::CreateWinGetSourceUnit(source, sourceUnitType); + configContext.Set().Units().Append(sourceUnit); for (const auto& package : source.Packages) { @@ -1857,12 +1868,8 @@ namespace AppInstaller::CLI::Workflow // There should be 1 package under 1 source. THROW_HR_IF(E_UNEXPECTED, exportSources.size() != 1 || exportSources[0].Packages.size() != 1); - std::optional sourceUnit; - if (!CheckForWellKnownSource(exportSources[0].Details)) - { - sourceUnit = anon::CreateWinGetSourceUnit(exportSources[0], GetWinGetSourceUnitType(configContext)); - configContext.Set().Units().Append(sourceUnit.value()); - } + ConfigurationUnit sourceUnit = anon::CreateWinGetSourceUnit(exportSources[0], GetWinGetSourceUnitType(configContext)); + configContext.Set().Units().Append(sourceUnit); singlePackageUnit = anon::CreateWinGetPackageUnit(exportSources[0].Packages[0], exportSources[0], context.Args.Contains(Args::Type::IncludeVersions), sourceUnit, GetWinGetPackageUnitType(configContext)); configContext.Set().Units().Append(singlePackageUnit.value()); diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp index 1f34512e61..54f71d2522 100644 --- a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -119,7 +119,6 @@ namespace AppInstaller::CLI::Workflow std::string_view name = context.Args.GetArg(Args::Type::SourceName); std::string_view arg = context.Args.GetArg(Args::Type::SourceArg); std::string_view type = context.Args.GetArg(Args::Type::SourceType); - bool isExplicit = context.Args.Contains(Args::Type::SourceExplicit); Repository::SourceTrustLevel trustLevel = Repository::SourceTrustLevel::None; if (context.Args.Contains(Execution::Args::Type::SourceTrustLevel)) @@ -128,7 +127,19 @@ namespace AppInstaller::CLI::Workflow trustLevel = Repository::ConvertToSourceTrustLevelFlag(trustLevelArgs); } - Repository::Source sourceToAdd{ name, arg, type, trustLevel, isExplicit}; + Repository::SourceEdit additionalProperties; + + if (context.Args.Contains(Args::Type::SourceExplicit)) + { + additionalProperties.Explicit = true; + } + + if (context.Args.Contains(Args::Type::SourcePriority)) + { + additionalProperties.Priority = Utility::TryConvertStringToInt32(context.Args.GetArg(Args::Type::SourcePriority)); + } + + Repository::Source sourceToAdd{ name, arg, type, trustLevel, additionalProperties}; if (context.Args.Contains(Execution::Args::Type::CustomHeader)) { @@ -177,7 +188,11 @@ namespace AppInstaller::CLI::Workflow table.OutputLine({ Resource::LocString(Resource::String::SourceListData), source.Data }); table.OutputLine({ Resource::LocString(Resource::String::SourceListIdentifier), source.Identifier }); table.OutputLine({ Resource::LocString(Resource::String::SourceListTrustLevel), Repository::GetSourceTrustLevelForDisplay(source.TrustLevel)}); - table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(source.Explicit) }}); + table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(source.Explicit) } }); + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SourcePriority)) + { + table.OutputLine({ Resource::LocString(Resource::String::SourceListPriority), std::to_string(source.Priority) }); + } if (source.LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) { @@ -282,14 +297,20 @@ namespace AppInstaller::CLI::Workflow // Get the current source with this name. Repository::Source targetSource{ sd.Name }; auto oldExplicitValue = sd.Explicit; + auto oldPriorityValue = sd.Priority; + + Repository::SourceEdit edits; - std::optional isExplicit; if (context.Args.Contains(Execution::Args::Type::SourceEditExplicit)) { - isExplicit = Utility::TryConvertStringToBool(context.Args.GetArg(Execution::Args::Type::SourceEditExplicit)); + edits.Explicit = Utility::TryConvertStringToBool(context.Args.GetArg(Execution::Args::Type::SourceEditExplicit)); + } + + if (context.Args.Contains(Execution::Args::Type::SourcePriority)) + { + edits.Priority = Utility::TryConvertStringToInt32(context.Args.GetArg(Execution::Args::Type::SourcePriority)); } - Repository::SourceEdit edits{ isExplicit }; if (!targetSource.RequiresChanges(edits)) { context.Reporter.Info() << Resource::String::SourceEditNoChanges(Utility::LocIndView{ sd.Name }) << std::endl; @@ -299,9 +320,19 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::SourceEditOne(Utility::LocIndView{ sd.Name }) << std::endl; targetSource.Edit(edits); - // Output updated source information. Since only Explicit is editable, we will only list that field. The name of the source being edited is listed prior to the edits. + // Output changed source information table. The name of the source being edited is listed prior to the edits. Execution::TableOutput<3> table(context.Reporter, { Resource::String::SourceListField, Resource::String::SourceEditOldValue, Resource::String::SourceEditNewValue }); - table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(oldExplicitValue) }, std::string{ Utility::ConvertBoolToString(isExplicit.value()) } }); + + if (edits.Explicit) + { + table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(oldExplicitValue) }, std::string{ Utility::ConvertBoolToString(edits.Explicit.value()) } }); + } + + if (edits.Priority) + { + table.OutputLine({ Resource::LocString(Resource::String::SourceListPriority), std::to_string(oldPriorityValue), std::to_string(edits.Priority.value()) }); + } + table.Complete(); } } @@ -364,6 +395,12 @@ namespace AppInstaller::CLI::Workflow std::vector sourceTrustLevels = Repository::SourceTrustLevelFlagToList(source.TrustLevel); s.TrustLevel = std::vector(sourceTrustLevels.begin(), sourceTrustLevels.end()); s.Explicit = source.Explicit; + + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SourcePriority)) + { + s.Priority = source.Priority; + } + context.Reporter.Info() << s.ToJsonString() << std::endl; } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 10850b3a40..09b6e2b100 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -1310,11 +1310,54 @@ namespace AppInstaller::CLI::Workflow { auto& searchResult = context.Get(); + bool operationTargetsInstalled = m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || + m_operationType == OperationType::Repair || m_operationType == OperationType::Export; + + // Try limiting results to highest priority sources + if (searchResult.Matches.size() > 1 && !operationTargetsInstalled && + ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SourcePriority)) + { + // Find the set of matches that have the highest priority + std::vector highestPriorityMatches; + std::optional highestPriority; + + for (const auto& match : searchResult.Matches) + { + std::optional priority = GetSourcePriority(match.Package); + + // Optional provides overloads that make empty less than valued and empties equal. + if (highestPriority < priority) + { + // Current priority is higher; reset. + highestPriority = priority; + highestPriorityMatches.clear(); + } + else if (highestPriority == priority) + { + // Priority is equal, add to the list. + } + else + { + // Current priority is lower, ignore the match. + continue; + } + + highestPriorityMatches.emplace_back(match); + } + + if (highestPriorityMatches.size() < searchResult.Matches.size()) + { + AICLI_LOG(CLI, Info, << "Replacing search results with only those from the highest priority [" << (highestPriority ? std::to_string(highestPriority.value()) : "none"s) << "]."); + searchResult.Matches = std::move(highestPriorityMatches); + context.Reporter.Warn() << Resource::String::MultiplePackagesFoundFilteredBySourcePriority << std::endl; + } + } + if (searchResult.Matches.size() > 1) { Logging::Telemetry().LogMultiAppMatch(); - if (m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || m_operationType == OperationType::Repair || m_operationType == OperationType::Export) + if (operationTargetsInstalled) { context.Reporter.Warn() << Resource::String::MultipleInstalledPackagesFound << std::endl; context << ReportMultiplePackageFoundResult; diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs index ad48c68d50..dc5d9e7061 100644 --- a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -23,11 +23,13 @@ public class DSCv3SourceResourceCommand : DSCv3ResourceTestBase private const string DefaultTrustLevel = "none"; private const string TrustedTrustLevel = "trusted"; private const bool DefaultExplicitState = false; + private const int DefaultPriority = 0; private const string SourceResource = "source"; private const string ArgumentPropertyName = "argument"; private const string TypePropertyName = "type"; private const string TrustLevelPropertyName = "trustLevel"; private const string ExplicitPropertyName = "explicit"; + private const string PriorityPropertyName = "priority"; private static string DefaultSourceArgForCmdLine { @@ -76,6 +78,7 @@ public void OneTimeTeardown() public void Setup() { RemoveTestSource(); + WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } /// @@ -155,19 +158,21 @@ public void Source_Test_SimplePresent() } /// - /// Calls `test` on the `source` resource with a argument that matches. + /// Calls `test` on the `source` resource with an argument that matches. /// /// The argument to use when adding the existing source. /// The trust level to use when adding the existing source. /// The explicit state to use when adding the existing source. + /// The priority to use when adding the existing source. /// The property to target for the test. - [TestCase(false, DefaultTrustLevel, true, ArgumentPropertyName)] - [TestCase(true, DefaultTrustLevel, false, TypePropertyName)] - [TestCase(false, TrustedTrustLevel, false, TrustLevelPropertyName)] - [TestCase(true, DefaultTrustLevel, true, ExplicitPropertyName)] - public void Source_Test_PropertyMatch(bool useDefaultArgument, string trustLevel, bool isExplicit, string targetProperty) + [TestCase(false, DefaultTrustLevel, true, 42, ArgumentPropertyName)] + [TestCase(true, DefaultTrustLevel, false, 14, TypePropertyName)] + [TestCase(false, TrustedTrustLevel, false, 42, TrustLevelPropertyName)] + [TestCase(true, DefaultTrustLevel, true, 39, ExplicitPropertyName)] + [TestCase(true, DefaultTrustLevel, true, 1, PriorityPropertyName)] + public void Source_Test_PropertyMatch(bool useDefaultArgument, string trustLevel, bool isExplicit, int priority, string targetProperty) { - var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)}"); + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)} --priority {priority}"); Assert.AreEqual(0, setup.ExitCode); SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; @@ -186,6 +191,9 @@ public void Source_Test_PropertyMatch(bool useDefaultArgument, string trustLevel case ExplicitPropertyName: resourceData.Explicit = isExplicit; break; + case PriorityPropertyName: + resourceData.Priority = priority; + break; default: Assert.Fail($"{targetProperty} is not a handled case."); break; @@ -207,14 +215,16 @@ public void Source_Test_PropertyMatch(bool useDefaultArgument, string trustLevel /// The argument to use when adding the existing source. /// The trust level to use when adding the existing source. /// The explicit state to use when adding the existing source. + /// The priority to use when adding the existing source. /// The property to target for the test. /// The value to test against. - [TestCase(false, DefaultTrustLevel, true, ArgumentPropertyName, true)] - [TestCase(false, DefaultTrustLevel, false, TrustLevelPropertyName, TrustedTrustLevel)] - [TestCase(true, DefaultTrustLevel, true, ExplicitPropertyName, false)] - public void Source_Test_PropertyMismatch(bool useDefaultArgument, string trustLevel, bool isExplicit, string targetProperty, object testValue) + [TestCase(false, DefaultTrustLevel, true, 2, ArgumentPropertyName, true)] + [TestCase(false, DefaultTrustLevel, false, 13, TrustLevelPropertyName, TrustedTrustLevel)] + [TestCase(true, DefaultTrustLevel, true, 42, ExplicitPropertyName, false)] + [TestCase(true, DefaultTrustLevel, true, 8, PriorityPropertyName, 76)] + public void Source_Test_PropertyMismatch(bool useDefaultArgument, string trustLevel, bool isExplicit, int priority, string targetProperty, object testValue) { - var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)}"); + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)} --priority {priority}"); Assert.AreEqual(0, setup.ExitCode); SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; @@ -230,6 +240,9 @@ public void Source_Test_PropertyMismatch(bool useDefaultArgument, string trustLe case ExplicitPropertyName: resourceData.Explicit = (bool)testValue; break; + case PriorityPropertyName: + resourceData.Priority = (int)testValue; + break; default: Assert.Fail($"{targetProperty} is not a handled case."); break; @@ -251,7 +264,7 @@ public void Source_Test_PropertyMismatch(bool useDefaultArgument, string trustLe [Test] public void Source_Test_AllMatch() { - var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {NonDefaultSourceArgForCmdLine} --type {DefaultSourceType} --trust-level {TrustedTrustLevel} --explicit"); + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {NonDefaultSourceArgForCmdLine} --type {DefaultSourceType} --trust-level {TrustedTrustLevel} --explicit --priority 42"); Assert.AreEqual(0, setup.ExitCode); SourceResourceData resourceData = new SourceResourceData() @@ -261,6 +274,7 @@ public void Source_Test_AllMatch() Type = DefaultSourceType, TrustLevel = TrustedTrustLevel, Explicit = true, + Priority = 42, }; var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); @@ -383,6 +397,43 @@ public void Source_Set_Replace() AssertExistingSourceResourceData(output, resourceData); } + /// + /// Calls `set` on the `source` resource with an existing item, editing it due to only changing editable properties. + /// + [Test] + public void Source_Set_Replace_Edit() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType}"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Explicit = true, + Priority = 42, + }; + + var result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + + AssertDiffState(diff, [ExplicitPropertyName, PriorityPropertyName]); + + // Call `get` to ensure the result + SourceResourceData resourceDataForGet = new SourceResourceData() + { + Name = DefaultSourceName, + }; + + result = RunDSCv3Command(SourceResource, GetFunction, resourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + } + /// /// Calls `export` on the `source` resource without providing any input. /// @@ -408,6 +459,7 @@ public void Source_Export_NoInput() Assert.AreEqual(DefaultSourceType, item.Type); Assert.AreEqual(DefaultTrustLevel, item.TrustLevel); Assert.AreEqual(DefaultExplicitState, item.Explicit); + Assert.AreEqual(DefaultPriority, item.Priority); break; } } @@ -429,15 +481,20 @@ private static void RemoveTestSource() private static void AssertExistingSourceResourceData(SourceResourceData output, SourceResourceData input) { - AssertExistingSourceResourceData(output, input.Argument, input.TrustLevel, input.Explicit); + AssertExistingSourceResourceData(output, input.Argument, input.TrustLevel, input.Explicit, input.Priority); } - private static void AssertExistingSourceResourceData(SourceResourceData output, string argument, string trustLevel = null, bool? isExplicit = null) + private static void AssertExistingSourceResourceData(SourceResourceData output, string argument, string trustLevel = null, bool? isExplicit = null, int? priority = null) { Assert.IsNotNull(output); Assert.True(output.Exist); Assert.AreEqual(DefaultSourceName, output.Name); - Assert.AreEqual(argument, output.Argument); + + if (argument != null) + { + Assert.AreEqual(argument, output.Argument); + } + Assert.AreEqual(DefaultSourceType, output.Type); if (trustLevel != null) @@ -449,6 +506,11 @@ private static void AssertExistingSourceResourceData(SourceResourceData output, { Assert.AreEqual(isExplicit, output.Explicit); } + + if (priority != null) + { + Assert.AreEqual(priority, output.Priority); + } } private static string CreateSourceArgument(bool forCommandLine = false, int openHR = 0, int searchHR = 0) @@ -477,6 +539,8 @@ private class SourceResourceData public bool? Explicit { get; set; } public bool? AcceptAgreements { get; set; } + + public int? Priority { get; set; } } } } diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index dea4021ef5..aa3420782e 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -227,6 +227,7 @@ public static void InitializeAllFeatures(bool status) ConfigureFeature(settingsJson, "resume", status); ConfigureFeature(settingsJson, "reboot", status); ConfigureFeature(settingsJson, "fonts", status); + ConfigureFeature(settingsJson, "sourcePriority", status); SetWingetSettings(settingsJson); } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 99c3a0c122..e5de1868e1 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -16,6 +16,15 @@ namespace AppInstallerCLIE2ETests /// public class InstallCommand : BaseCommand { + /// + /// One time set up. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); + } + /// /// Set up. /// @@ -797,5 +806,37 @@ public void InstallExeThatInstallsMSIX() TestCommon.RemoveARPEntry(fakeProductCode); } + + /// + /// Test install source priority. + /// + [Test] + public void InstallExeWithSourcePriority() + { + // This test source always returns a single package from search + TestCommon.RunAICLICommand("source add", "dummyPackage \"{ \"\"ContainsPackage\"\": true }\" Microsoft.Test.Configurable --header \"{}\""); + + try + { + // Attempt install with equal (default) priorities + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_MULTIPLE_APPLICATIONS_FOUND, result.ExitCode); + Assert.False(TestCommon.VerifyTestExeInstalledAndCleanup(installDir)); + + // Change the priority of the primary test source to be higher + TestCommon.RunAICLICommand("source edit", $"{Constants.TestSourceName} --priority 1"); + + result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir)); + } + finally + { + this.ResetTestSource(); + } + } } } diff --git a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs index 7b7df12ed0..b312e4840d 100644 --- a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs @@ -292,6 +292,7 @@ public async Task AddEditRemovePackageCatalog() options.Name = Constants.TestSourceName; options.TrustLevel = PackageCatalogTrustLevel.Trusted; options.Explicit = true; + options.Priority = 12; await this.AddAndValidatePackageCatalogAsync(options, AddPackageCatalogStatus.Ok); @@ -299,6 +300,7 @@ public async Task AddEditRemovePackageCatalog() EditPackageCatalogOptions editOptions = this.TestFactory.CreateEditPackageCatalogOptions(); editOptions.Name = Constants.TestSourceName; editOptions.Explicit = false; + editOptions.Priority = 42; this.EditAndValidatePackageCatalog(editOptions, EditPackageCatalogStatus.Ok); // Remove @@ -342,6 +344,7 @@ private PackageCatalogReference GetAndValidatePackageCatalog(AddPackageCatalogOp Assert.AreEqual(addPackageCatalogOptions.Name, packageCatalog.Info.Name); Assert.AreEqual(addPackageCatalogOptions.SourceUri, packageCatalog.Info.Argument); Assert.AreEqual(addPackageCatalogOptions.Explicit, packageCatalog.Info.Explicit); + Assert.AreEqual(addPackageCatalogOptions.Priority, packageCatalog.Info.Priority); return packageCatalog; } @@ -402,6 +405,11 @@ private void EditAndValidatePackageCatalog(EditPackageCatalogOptions editPackage { Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit); } + + if (editPackageCatalogOptions.Priority != null) + { + Assert.AreEqual(packageCatalog.Info.Priority, editPackageCatalogOptions.Priority); + } } } } diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index 4f60b94455..ee4207e366 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -20,7 +20,7 @@ public class SourceCommand : BaseCommand [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); + WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } /// @@ -106,6 +106,28 @@ public void SourceAddWithExplicit() TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); } + /// + /// Test source add with a priority value. + /// + [Test] + public void SourceAddWithPriority() + { + // Remove the test source. + TestCommon.RunAICLICommand("source remove", Constants.TestSourceName); + + var result = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --priority 42"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Done")); + + var listResult = TestCommon.RunAICLICommand("source list", "SourceTest"); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); + Assert.True(listResult.StdOut.Contains("42")); + + var exportResult = TestCommon.RunAICLICommand("source export", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); + Assert.True(exportResult.StdOut.Contains("42")); + } + /// /// Test source add with duplicate name. /// @@ -269,7 +291,7 @@ public void SourceForceReset() } /// - /// Test source add with explicit flag, edit the source to not be explicit. + /// Test source edit with explicit flag, edit the source to not be explicit. /// [Test] public void SourceEdit() @@ -304,6 +326,30 @@ public void SourceEdit() TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); } + /// + /// Test source edit with priority. + /// + [Test] + public void SourceEdit_Priority() + { + // Remove the test source. + TestCommon.RunAICLICommand("source remove", Constants.TestSourceName); + + var addResult = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, addResult.ExitCode); + Assert.True(addResult.StdOut.Contains("Done")); + + // Run the edit, this should be S_OK with "Done" as it changed the state + var editResult = TestCommon.RunAICLICommand("source edit", $"SourceTest --priority 14"); + Assert.AreEqual(Constants.ErrorCode.S_OK, editResult.ExitCode); + Assert.True(editResult.StdOut.Contains("14")); + + // Run it again, this should result in S_OK with no changes and a message that the source is already in that state. + var editResult2 = TestCommon.RunAICLICommand("source edit", $"SourceTest --priority 14"); + Assert.AreEqual(Constants.ErrorCode.S_OK, editResult2.ExitCode); + Assert.True(editResult2.StdOut.Contains("The source named 'SourceTest' is already in the desired state.")); + } + /// /// Test override of a default source via edit command. /// diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index f9e4a768f3..46eee78281 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3535,4 +3535,19 @@ An unlocalized JSON fragment will follow on another line. New Value Column title for listing the new value. + + Priority; higher numbers first + This argument sets the numerical priority of the source. Higher values will be first in priority. + + + Priority + Label for the priority of the source with respect to other sources. Higher values come first in the order. + + + The priority of the source. Higher values are sorted first in the order. + + + Results have been filtered to the highest matched source priority. + A warning message to indicate to the user that the results of a search have been filtered by choosing only those matching the highest source priority amongst the results. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index e2d439a275..7e9f524056 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -258,6 +258,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 643382697d..d04de5b6a6 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -395,6 +395,9 @@ Source Files\Common + + Source Files\Repository + diff --git a/src/AppInstallerCLITests/MatchCriteriaResolver.cpp b/src/AppInstallerCLITests/MatchCriteriaResolver.cpp new file mode 100644 index 0000000000..998884fd75 --- /dev/null +++ b/src/AppInstallerCLITests/MatchCriteriaResolver.cpp @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestHooks.h" +#include "TestSource.h" +#include "AppInstallerStrings.h" +#include "MatchCriteriaResolver.h" + +using namespace AppInstaller; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; +using namespace TestCommon; + + +void RequireMatchCriteria(const PackageMatchFilter& expected, const PackageMatchFilter& actual) +{ + REQUIRE(expected.Field == actual.Field); + REQUIRE(expected.Type == actual.Type); + REQUIRE(expected.Value == actual.Value); +} + +TEST_CASE("MatchCriteriaResolver_MatchType", "[MatchCriteriaResolver]") +{ + Manifest::Manifest manifest; + PackageMatchFilter expected{ PackageMatchField::Id, MatchType::Wildcard, "Not set by test" }; + std::string searchString = "Search"; + + SECTION("Exact") + { + manifest.Id = searchString; + expected.Type = MatchType::Exact; + } + SECTION("Case Insensitive") + { + manifest.Id = "search"; + expected.Type = MatchType::CaseInsensitive; + } + SECTION("Starts With") + { + manifest.Id = "Search Result"; + expected.Type = MatchType::StartsWith; + } + SECTION("Substring") + { + manifest.Id = "Contains searches within"; + expected.Type = MatchType::Substring; + } + SECTION("None") + { + expected.Field = PackageMatchField::Unknown; + } + + expected.Value = manifest.Id; + + SearchRequest request; + request.Query = RequestMatch{ MatchType::Substring, searchString }; + + TestPackageVersion packageVersion(manifest); + + PackageMatchFilter actual = FindBestMatchCriteria(request, &packageVersion); + RequireMatchCriteria(expected, actual); +} + +TEST_CASE("MatchCriteriaResolver_MatchField", "[MatchCriteriaResolver]") +{ + Manifest::Manifest manifest; + Utility::NormalizedString searchString = "Search"; + auto foldedSearchString = Utility::FoldCase(searchString); + PackageMatchFilter expected{ PackageMatchField::Unknown, MatchType::Exact, searchString }; + + SECTION("Identifier") + { + manifest.Id = searchString; + expected.Field = PackageMatchField::Id; + } + SECTION("Name") + { + manifest.DefaultLocalization.Add(searchString); + expected.Field = PackageMatchField::Name; + } + SECTION("Moniker") + { + manifest.Moniker = searchString; + expected.Field = PackageMatchField::Moniker; + } + SECTION("Command") + { + manifest.Installers.emplace_back().Commands.emplace_back(searchString); + expected.Field = PackageMatchField::Command; + } + SECTION("Tag") + { + manifest.DefaultLocalization.Add({ searchString }); + expected.Field = PackageMatchField::Tag; + } + SECTION("Package Family Name") + { + manifest.Installers.emplace_back().PackageFamilyName = searchString; + expected.Field = PackageMatchField::PackageFamilyName; + // Folded by test package version + expected.Type = MatchType::CaseInsensitive; + expected.Value = foldedSearchString; + } + SECTION("Product Code") + { + manifest.Installers.emplace_back().ProductCode = searchString; + expected.Field = PackageMatchField::ProductCode; + // Folded by test package version + expected.Type = MatchType::CaseInsensitive; + expected.Value = foldedSearchString; + } + SECTION("Upgrade Code") + { + manifest.Installers.emplace_back().AppsAndFeaturesEntries.emplace_back().UpgradeCode = searchString; + expected.Field = PackageMatchField::UpgradeCode; + // Folded by test package version + expected.Type = MatchType::CaseInsensitive; + expected.Value = foldedSearchString; + } + + SearchRequest request; + request.Query = RequestMatch{ MatchType::Substring, searchString }; + + TestPackageVersion packageVersion(manifest); + + PackageMatchFilter actual = FindBestMatchCriteria(request, &packageVersion); + RequireMatchCriteria(expected, actual); +} + +TEST_CASE("MatchCriteriaResolver_Complex", "[MatchCriteriaResolver]") +{ + Manifest::Manifest manifest; + Utility::NormalizedString searchString = "Search"; + auto foldedSearchString = Utility::FoldCase(searchString); + PackageMatchFilter expected{ PackageMatchField::Tag, MatchType::Exact, searchString }; + + manifest.Id = "Identifer search substring"; + manifest.DefaultLocalization.Add("Search name starts"); + manifest.Moniker = foldedSearchString; + manifest.Installers.emplace_back().Commands.emplace_back("Command search string"); + manifest.DefaultLocalization.Add({ searchString }); + manifest.Installers.emplace_back().PackageFamilyName = searchString; + manifest.Installers.emplace_back().ProductCode = searchString; + manifest.Installers.emplace_back().AppsAndFeaturesEntries.emplace_back().UpgradeCode = searchString; + + SearchRequest request; + request.Query = RequestMatch{ MatchType::Substring, searchString }; + + TestPackageVersion packageVersion(manifest); + + PackageMatchFilter actual = FindBestMatchCriteria(request, &packageVersion); + RequireMatchCriteria(expected, actual); +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index 7c0a31dfeb..a92683ce71 100644 --- a/src/AppInstallerCLITests/Sources.cpp +++ b/src/AppInstallerCLITests/Sources.cpp @@ -75,6 +75,7 @@ constexpr std::string_view s_SingleSourceOverride = R"( IsTombstone: false IsOverride: true Explicit: false + Priority: 12 )"sv; constexpr std::string_view s_SingleSourceMetadata = R"( @@ -118,16 +119,19 @@ constexpr std::string_view s_ThreeSources = R"( Arg: testArg Data: testData IsTombstone: false + Priority: 1 - Name: testName2 Type: testType2 Arg: testArg2 Data: testData2 IsTombstone: false + Priority: 5 - Name: testName3 Type: testType3 Arg: testArg3 Data: testData3 IsTombstone: false + Priority: 3 - Name: winget Type: "" Arg: "" @@ -195,7 +199,7 @@ constexpr std::string_view s_UserSourceNamedLikeDefault = R"( IsTombstone: false )"sv; -constexpr std::string_view s_SingleSource_TrustLevels_Explicit= R"( +constexpr std::string_view s_SingleSource_AllProperties= R"( Sources: - Name: testName Type: testType @@ -204,6 +208,7 @@ constexpr std::string_view s_SingleSource_TrustLevels_Explicit= R"( IsTombstone: false TrustLevel: 3 Explicit: true + Priority: 1 )"sv; namespace @@ -317,6 +322,7 @@ TEST_CASE("RepoSources_DefaultSourceOverride", "[sources]") REQUIRE(beforeOverride[2].Type == "Microsoft.PreIndexed.Package"); REQUIRE(beforeOverride[2].Origin == SourceOrigin::Default); REQUIRE(beforeOverride[2].Explicit == true); + REQUIRE(beforeOverride[2].Priority == 0); SetSetting(Stream::UserSources, s_SingleSourceOverride); auto afterOverride = GetSources(); @@ -334,6 +340,7 @@ TEST_CASE("RepoSources_DefaultSourceOverride", "[sources]") // The only properties we expect to be different are the Origin, which is now User, and Explicit. REQUIRE(afterOverride[0].Origin == SourceOrigin::User); REQUIRE(afterOverride[0].Explicit == false); + REQUIRE(afterOverride[0].Priority == 12); } TEST_CASE("RepoSources_SingleSource", "[sources]") @@ -354,9 +361,9 @@ TEST_CASE("RepoSources_SingleSource", "[sources]") RequireDefaultSourcesAt(sources, 1); } -TEST_CASE("RepoSources_SingleSource_TrustLevel_Explicit", "[sources]") +TEST_CASE("RepoSources_SingleSource_AllProperties", "[sources]") { - SetSetting(Stream::UserSources, s_SingleSource_TrustLevels_Explicit); + SetSetting(Stream::UserSources, s_SingleSource_AllProperties); RemoveSetting(Stream::SourcesMetadata); std::vector sources = GetSources(); @@ -368,6 +375,7 @@ TEST_CASE("RepoSources_SingleSource_TrustLevel_Explicit", "[sources]") REQUIRE(sources[0].Data == "testData"); REQUIRE(sources[0].Origin == SourceOrigin::User); REQUIRE(sources[0].Explicit == true); + REQUIRE(sources[0].Priority == 1); REQUIRE(WI_IsFlagSet(sources[0].TrustLevel, SourceTrustLevel::Trusted)); REQUIRE(WI_IsFlagSet(sources[0].TrustLevel, SourceTrustLevel::StoreOrigin)); REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); @@ -380,20 +388,36 @@ TEST_CASE("RepoSources_ThreeSources", "[sources]") SetSetting(Stream::UserSources, s_ThreeSources); SetSetting(Stream::SourcesMetadata, s_ThreeSourcesMetadata); + const char* suffixStrings[3] = { "", "2", "3" }; + size_t suffixUnsorted[3] = { 0, 1, 2 }; + size_t suffixPrioritySorted[3] = { 1, 2, 0 }; + size_t* suffix = nullptr; + std::unique_ptr override; + + SECTION("Unsorted") + { + suffix = suffixUnsorted; + } + SECTION("Priority Sorted") + { + override = std::make_unique(ExperimentalFeature::Feature::SourcePriority); + suffix = suffixPrioritySorted; + } + std::vector sources = GetSources(); REQUIRE(sources.size() == 3); - const char* suffix[3] = { "", "2", "3" }; - - for (size_t i = 0; i < 3; ++i) + for (size_t index = 0; index < 3; ++index) { - INFO("Source #" << i); - REQUIRE(sources[i].Name == "testName"s + suffix[i]); - REQUIRE(sources[i].Type == "testType"s + suffix[i]); - REQUIRE(sources[i].Arg == "testArg"s + suffix[i]); - REQUIRE(sources[i].Data == "testData"s + suffix[i]); - REQUIRE(sources[i].LastUpdateTime == ConvertUnixEpochToSystemClock(i)); - REQUIRE(sources[i].Origin == SourceOrigin::User); + size_t i = suffix[index]; + + INFO("Source #" << index << " [" << i << "]"); + REQUIRE(sources[index].Name == "testName"s + suffixStrings[i]); + REQUIRE(sources[index].Type == "testType"s + suffixStrings[i]); + REQUIRE(sources[index].Arg == "testArg"s + suffixStrings[i]); + REQUIRE(sources[index].Data == "testData"s + suffixStrings[i]); + REQUIRE(sources[index].LastUpdateTime == ConvertUnixEpochToSystemClock(i)); + REQUIRE(sources[index].Origin == SourceOrigin::User); } } @@ -423,6 +447,7 @@ TEST_CASE("RepoSources_AddSource", "[sources]") details.Data = "thisIsTheData"; details.TrustLevel = Repository::SourceTrustLevel::None; details.Explicit = false; + details.Priority = 42; bool addCalledOnFactory = false; TestSourceFactory factory{ SourcesTestSource::Create }; @@ -445,6 +470,7 @@ TEST_CASE("RepoSources_AddSource", "[sources]") REQUIRE(sources[0].Origin == SourceOrigin::User); REQUIRE(sources[0].TrustLevel == details.TrustLevel); REQUIRE(sources[0].Explicit == details.Explicit); + REQUIRE(sources[0].Priority == details.Priority); RequireDefaultSourcesAt(sources, 1); } @@ -1340,7 +1366,7 @@ TEST_CASE("RepoSources_RestoringWellKnownSource", "[sources]") SECTION("with well known name") { - Source addStoreBack{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; + Source addStoreBack{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, {} }; REQUIRE(addStoreBack.Add(progress)); Source storeAfterAdd{ details.Name }; @@ -1351,7 +1377,7 @@ TEST_CASE("RepoSources_RestoringWellKnownSource", "[sources]") SECTION("with different name") { std::string newName = details.Name + "_new"; - Source addStoreBack{ newName, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; + Source addStoreBack{ newName, details.Arg, details.Type, Repository::SourceTrustLevel::None, {} }; REQUIRE(addStoreBack.Add(progress)); Source storeAfterAdd{ newName }; diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index bcd624dc92..44c5b2304a 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include "TestSettings.h" + #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +62,7 @@ namespace AppInstaller namespace Settings { void SetUserSettingsOverride(UserSettings* value); + void SetExperimentalFeatureOverride(const std::map* override); } namespace Filesystem @@ -356,4 +360,34 @@ namespace TestHook private: }; + + struct SetUserSettings_Override + { + SetUserSettings_Override(AppInstaller::Settings::UserSettings& settings) + { + AppInstaller::Settings::SetUserSettingsOverride(&settings); + } + + ~SetUserSettings_Override() + { + AppInstaller::Settings::SetUserSettingsOverride(nullptr); + } + }; + + struct SetSingleExperimentalFeature_Override + { + SetSingleExperimentalFeature_Override(AppInstaller::Settings::ExperimentalFeature::Feature feature) + { + m_overrides[feature] = true; + AppInstaller::Settings::SetExperimentalFeatureOverride(&m_overrides); + } + + ~SetSingleExperimentalFeature_Override() + { + AppInstaller::Settings::SetExperimentalFeatureOverride(nullptr); + } + + private: + std::map m_overrides; + }; } diff --git a/src/AppInstallerCLITests/TestSettings.cpp b/src/AppInstallerCLITests/TestSettings.cpp index 20613c93fa..2ee0b0dc1a 100644 --- a/src/AppInstallerCLITests/TestSettings.cpp +++ b/src/AppInstallerCLITests/TestSettings.cpp @@ -71,4 +71,4 @@ namespace TestCommon { m_toggles[policy] = state; } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/TestSettings.h b/src/AppInstallerCLITests/TestSettings.h index 4fe1ed718d..17f08fe748 100644 --- a/src/AppInstallerCLITests/TestSettings.h +++ b/src/AppInstallerCLITests/TestSettings.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include "TestCommon.h" #include #include #include diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index 6544f99395..e900dc556a 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -57,6 +57,8 @@ namespace TestCommon return LocIndString{ VersionManifest.GetArpVersionRange().IsEmpty() ? "" : VersionManifest.GetArpVersionRange().GetMinVersion().ToString() }; case PackageVersionProperty::ArpMaxVersion: return LocIndString{ VersionManifest.GetArpVersionRange().IsEmpty() ? "" : VersionManifest.GetArpVersionRange().GetMaxVersion().ToString() }; + case PackageVersionProperty::Moniker: + return LocIndString{ VersionManifest.Moniker }; default: return {}; } @@ -106,6 +108,31 @@ namespace TestCommon result.emplace_back(loc.Locale); } break; + case PackageVersionMultiProperty::Command: + for (auto value : VersionManifest.GetAggregatedCommands()) + { + result.emplace_back(std::move(value)); + } + break; + case PackageVersionMultiProperty::Tag: + for (auto value : VersionManifest.GetAggregatedTags()) + { + result.emplace_back(std::move(value)); + } + break; + case PackageVersionMultiProperty::UpgradeCode: + if (!HideSystemReferenceStrings) + { + for (const auto& installer : VersionManifest.Installers) + { + bool shouldFoldCaseForNonPortable = installer.EffectiveInstallerType() != AppInstaller::Manifest::InstallerTypeEnum::Portable; + for (const auto& entry : installer.AppsAndFeaturesEntries) + { + AddIfHasValueAndNotPresent(entry.UpgradeCode, result, shouldFoldCaseForNonPortable); + } + } + } + break; } return result; @@ -462,7 +489,10 @@ namespace TestCommon bool AddSource(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback& progress) { - Repository::Source source{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; + Repository::SourceEdit additionalProperties; + additionalProperties.Explicit = details.Explicit; + additionalProperties.Priority = details.Priority; + Repository::Source source{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, additionalProperties }; return source.Add(progress); } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index d417a6645e..dd659448a8 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -8,8 +8,18 @@ namespace AppInstaller::Settings { +#ifndef AICLI_DISABLE_TEST_HOOKS + const std::map* s_ExperimentalFeature_Override = nullptr; + + void SetExperimentalFeatureOverride(const std::map* override) + { + s_ExperimentalFeature_Override = override; + } +#endif + namespace { + bool IsEnabledInternal(ExperimentalFeature::Feature feature, const UserSettings& userSettings) { if (feature == ExperimentalFeature::Feature::None) @@ -22,6 +32,17 @@ namespace AppInstaller::Settings return false; #else +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_ExperimentalFeature_Override) + { + auto itr = s_ExperimentalFeature_Override->find(feature); + if (itr != s_ExperimentalFeature_Override->end()) + { + return itr->second; + } + } +#endif + if (!GroupPolicies().IsEnabled(TogglePolicy::Policy::ExperimentalFeatures)) { AICLI_LOG(Core, Info, << @@ -44,6 +65,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Font: return userSettings.Get(); + case ExperimentalFeature::Feature::SourcePriority: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -77,6 +100,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Font: return ExperimentalFeature{ "Font", "fonts", "https://aka.ms/winget-settings", Feature::Font }; + case Feature::SourcePriority: + return ExperimentalFeature{ "Source Priority", "sourcePriority", "https://aka.ms/winget-settings", Feature::SourcePriority }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 2dc097f548..42559317ef 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,6 +25,7 @@ namespace AppInstaller::Settings DirectMSI = 0x1, Resume = 0x2, Font = 0x4, + SourcePriority = 0x8, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 10b0c8f36b..95fbe73549 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFFonts, + EFSourcePriority, // Telemetry TelemetryDisable, // Install behavior @@ -163,6 +164,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFSourcePriority, bool, bool, false, ".experimentalFeatures.sourcePriority"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index f220ec3c2c..db276351ff 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -267,6 +267,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFFonts) + WINGET_VALIDATE_PASS_THROUGH(EFSourcePriority) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index d02ada420b..53a8260f9c 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -278,6 +278,7 @@ + @@ -411,6 +412,7 @@ + @@ -518,5 +520,4 @@ - - + \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 7e89c4bf56..d877dedeab 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -504,6 +504,9 @@ Rest + + Header Files + @@ -788,6 +791,9 @@ Rest + + Source Files + diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp new file mode 100644 index 0000000000..48074c62e9 --- /dev/null +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. +#include "pch.h" +#include "MatchCriteriaResolver.h" +#include + +namespace AppInstaller::Repository +{ + namespace + { + using ValueMatchFunction = bool (*)(const Utility::NormalizedString&, const Utility::NormalizedString&); + + bool ValueMatchFunction_AlwaysFalse(const Utility::NormalizedString&, const Utility::NormalizedString&) + { + return false; + } + + bool ValueMatchFunction_Exact(const Utility::NormalizedString& a, const Utility::NormalizedString& b) + { + return a == b; + } + + bool ValueMatchFunction_CaseInsensitive(const Utility::NormalizedString& a, const Utility::NormalizedString& b) + { + return Utility::ICUCaseInsensitiveEquals(a, b); + } + + bool ValueMatchFunction_StartsWith(const Utility::NormalizedString& a, const Utility::NormalizedString& b) + { + return Utility::ICUCaseInsensitiveStartsWith(a, b); + } + + bool ValueMatchFunction_Substring(const Utility::NormalizedString& a, const Utility::NormalizedString& b) + { + return Utility::ContainsSubstring(FoldCase(a), FoldCase(b)); + } + + ValueMatchFunction GetMatchTypeFunction(MatchType matchType) + { + switch (matchType) + { + case MatchType::Exact: + return ValueMatchFunction_Exact; + case MatchType::CaseInsensitive: + return ValueMatchFunction_CaseInsensitive; + case MatchType::StartsWith: + return ValueMatchFunction_StartsWith; + case MatchType::Substring: + return ValueMatchFunction_Substring; + case MatchType::Fuzzy: + case MatchType::FuzzySubstring: + case MatchType::Wildcard: + default: + return ValueMatchFunction_AlwaysFalse; + } + } + + PackageVersionProperty GetPackageVersionPropertyFor(PackageMatchField field) + { + switch (field) + { + case PackageMatchField::Id: + return PackageVersionProperty::Id; + case PackageMatchField::Name: + return PackageVersionProperty::Name; + case PackageMatchField::Moniker: + return PackageVersionProperty::Moniker; + default: + THROW_HR(E_UNEXPECTED); + } + } + + PackageVersionMultiProperty GetPackageVersionMultiPropertyFor(PackageMatchField field) + { + switch (field) + { + case PackageMatchField::Command: + return PackageVersionMultiProperty::Command; + case PackageMatchField::Tag: + return PackageVersionMultiProperty::Tag; + case PackageMatchField::PackageFamilyName: + return PackageVersionMultiProperty::PackageFamilyName; + case PackageMatchField::ProductCode: + return PackageVersionMultiProperty::ProductCode; + case PackageMatchField::UpgradeCode: + return PackageVersionMultiProperty::UpgradeCode; + default: + THROW_HR(E_UNEXPECTED); + } + } + + // Gets the best match type for the given field value and required minimum match type. + std::optional GetBestMatchType(const RequestMatch& request, MatchType mustBeBetterThanMatchType, const Utility::NormalizedString& value) + { + if (request.Value.empty()) + { + return std::nullopt; + } + + for (auto matchType : { MatchType::Exact, MatchType::CaseInsensitive, MatchType::StartsWith, MatchType::Substring }) + { + if (matchType >= mustBeBetterThanMatchType) + { + break; + } + + auto matchFunction = GetMatchTypeFunction(matchType); + + if (matchFunction(value, request.Value)) + { + return matchType; + } + } + + return std::nullopt; + } + + // Gets the best match type for the given field value and required minimum match type. + std::optional GetBestMatchType(const SearchRequest& request, PackageMatchField field, MatchType mustBeBetterThanMatchType, const Utility::NormalizedString& value) + { + std::optional result; + + if (request.Query) + { + result = GetBestMatchType(request.Query.value(), mustBeBetterThanMatchType, value); + + if (result) + { + mustBeBetterThanMatchType = result.value(); + } + } + + for (const auto& filter : request.Filters) + { + if (result.value_or(MatchType::Wildcard) == MatchType::Exact) + { + break; + } + + if (filter.Field == field) + { + std::optional filterResult = GetBestMatchType(filter, mustBeBetterThanMatchType, value); + + if (filterResult) + { + result = std::move(filterResult); + mustBeBetterThanMatchType = result.value(); + } + } + } + + return result; + } + + // Gets the best match and updates the result if it should be updated. + // Returns true to indicate that an exact match has been found. + bool UpdatePackageMatchFilterCheck(const SearchRequest& request, PackageMatchField field, PackageMatchFilter& result, const Utility::LocIndString& propertyValue) + { + Utility::NormalizedString normalizedValue = propertyValue.get(); + auto bestMatch = GetBestMatchType(request, field, result.Type, normalizedValue); + + if (bestMatch && bestMatch.value() < result.Type) + { + result.Type = bestMatch.value(); + result.Field = field; + result.Value = std::move(normalizedValue); + } + + return MatchType::Exact == result.Type; + } + } + + PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const IPackageVersion* packageVersion) + { + PackageMatchFilter result{ PackageMatchField::Unknown, MatchType::Wildcard }; + + // Single value fields + for (auto field : { PackageMatchField::Id, PackageMatchField::Name, PackageMatchField::Moniker }) + { + auto propertyValue = packageVersion->GetProperty(GetPackageVersionPropertyFor(field)); + if (propertyValue.empty()) + { + continue; + } + + if (UpdatePackageMatchFilterCheck(request, field, result, propertyValue)) + { + break; + } + } + + // Multi-value fields + for (auto field : { PackageMatchField::Command, PackageMatchField::Tag, PackageMatchField::PackageFamilyName, + PackageMatchField::ProductCode, PackageMatchField::UpgradeCode }) + { + if (MatchType::Exact == result.Type) + { + break; + } + + auto propertyValues = packageVersion->GetMultiProperty(GetPackageVersionMultiPropertyFor(field)); + if (propertyValues.empty()) + { + continue; + } + + for (const auto& propertyValue : propertyValues) + { + if (UpdatePackageMatchFilterCheck(request, field, result, propertyValue)) + { + break; + } + } + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h new file mode 100644 index 0000000000..6ad225275f --- /dev/null +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/winget/RepositorySearch.h" + +namespace AppInstaller::Repository +{ + // Finds the highest rated match criteria for the package based on the search request, + PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const IPackageVersion* packageVersion); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/ConfigurableTestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/ConfigurableTestSourceFactory.cpp index af8a17ebfe..58e182d4f8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ConfigurableTestSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/ConfigurableTestSourceFactory.cpp @@ -7,6 +7,7 @@ using namespace std::string_literals; using namespace std::string_view_literals; +using namespace AppInstaller::Utility::literals; namespace AppInstaller::Repository::Microsoft { @@ -29,6 +30,7 @@ namespace AppInstaller::Repository::Microsoft // TODO: If this becomes more dynamic, refactor the UserSettings code to make it easier to leverage here ParseHR(root, ".OpenHR", OpenHR); ParseHR(root, ".SearchHR", SearchHR); + ParseBool(root, ".ContainsPackage", ContainsPackage); } else { @@ -43,6 +45,9 @@ namespace AppInstaller::Repository::Microsoft // The HR to throw on Source::Search (if FAILED) HRESULT SearchHR = S_OK; + // If a result should be returned by search. + bool ContainsPackage = false; + private: static void ParseHR(const Json::Value& root, const std::string& path, HRESULT& hr) { @@ -60,10 +65,148 @@ namespace AppInstaller::Repository::Microsoft } } } + + static void ParseBool(const Json::Value& root, const std::string& path, bool& value) + { + const Json::Path jsonPath(path); + Json::Value node = jsonPath.resolve(root); + if (!node.isNull()) + { + if (node.isBool()) + { + value = node.asBool(); + } + } + } + }; + + // A test package that contains test data. + struct TestPackage : public std::enable_shared_from_this, public ICompositePackage, public IPackage, public IPackageVersion + { + TestPackage(std::shared_ptr source) : m_source(std::move(source)) {} + + Utility::LocIndString GetProperty(PackageProperty property) const override + { + switch (property) + { + case PackageProperty::Id: + return "ConfigurableTestSource Package Identifier"_lis; + case PackageProperty::Name: + return "ConfigurableTestSource Package Name"_lis; + } + + return {}; + } + + std::shared_ptr GetInstalled() override + { + return nullptr; + } + + std::vector> GetAvailable() override + { + return { shared_from_this() }; + } + + std::vector GetMultiProperty(PackageMultiProperty) const override + { + return {}; + } + + Source GetSource() const override + { + return { m_source }; + } + + bool IsSame(const IPackage*) const override + { + return false; + } + + const void* CastTo(IPackageType) const override + { + return nullptr; + } + + std::vector GetVersionKeys() const override + { + return { {} }; + } + + std::shared_ptr GetVersion(const PackageVersionKey&) const override + { + return std::static_pointer_cast(NonConstSharedFromThis()); + } + + std::shared_ptr GetLatestVersion() const override + { + return std::static_pointer_cast(NonConstSharedFromThis()); + } + + Utility::LocIndString GetProperty(PackageVersionProperty property) const override + { + switch (property) + { + case PackageVersionProperty::Id: + return "ConfigurableTestSource Package Version Identifier"_lis; + case PackageVersionProperty::Name: + return "ConfigurableTestSource Package Version Name"_lis; + case PackageVersionProperty::SourceIdentifier: + return "ConfigurableTestSource Package Version Source Identifier"_lis; + case PackageVersionProperty::SourceName: + return "ConfigurableTestSource Package Version Source Name"_lis; + case PackageVersionProperty::Version: + return "ConfigurableTestSource Package Version Version"_lis; + case PackageVersionProperty::Channel: + return "ConfigurableTestSource Package Version Channel"_lis; + case PackageVersionProperty::RelativePath: + return "ConfigurableTestSource Package Version Relative Path"_lis; + case PackageVersionProperty::ManifestSHA256Hash: + return "ConfigurableTestSource Package Version Manifest SHA 256 Hash"_lis; + case PackageVersionProperty::Publisher: + return "ConfigurableTestSource Package Version Publisher"_lis; + case PackageVersionProperty::ArpMinVersion: + return "ConfigurableTestSource Package Version Arp Min Version"_lis; + case PackageVersionProperty::ArpMaxVersion: + return "ConfigurableTestSource Package Version Arp Max Version"_lis; + case PackageVersionProperty::Moniker: + return "ConfigurableTestSource Package Version Moniker"_lis; + } + + return {}; + } + + std::vector GetMultiProperty(PackageVersionMultiProperty) const override + { + return {}; + } + + Manifest::Manifest GetManifest() override + { + Manifest::Manifest result; + + result.Id = "ConfigurableTestSource Manifest Identifier"; + result.CurrentLocalization.Add("ConfigurableTestSource Manifest Name"); + + return result; + } + + Metadata GetMetadata() const override + { + return {}; + } + + private: + std::shared_ptr NonConstSharedFromThis() const + { + return const_cast(this)->shared_from_this(); + } + + std::shared_ptr m_source; }; // The configurable source itself. - struct ConfigurableTestSource : public ISource + struct ConfigurableTestSource : public std::enable_shared_from_this, public ISource { static constexpr ISourceType SourceType = ISourceType::ConfigurableTestSource; @@ -77,7 +220,18 @@ namespace AppInstaller::Repository::Microsoft SearchResult Search(const SearchRequest&) const override { THROW_IF_FAILED(m_config.SearchHR); - return {}; + + SearchResult result; + + if (m_config.ContainsPackage) + { + std::shared_ptr package = std::make_shared(NonConstSharedFromThis()); + PackageMatchFilter packageFilter{ {}, {} }; + + result.Matches.emplace_back(std::move(package), std::move(packageFilter)); + } + + return result; } void* CastTo(ISourceType type) override @@ -91,6 +245,11 @@ namespace AppInstaller::Repository::Microsoft } private: + std::shared_ptr NonConstSharedFromThis() const + { + return const_cast(this)->shared_from_this(); + } + SourceDetails m_details; TestSourceConfiguration m_config; }; diff --git a/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp b/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp index 77cd3c7ac7..5303b6e248 100644 --- a/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp +++ b/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp @@ -276,4 +276,33 @@ namespace AppInstaller::Repository } } } + + std::optional GetSourcePriority(const std::shared_ptr& composite) + { + auto installed = composite->GetInstalled(); + + if (installed) + { + auto installedVersion = installed->GetLatestVersion(); + + if (installedVersion) + { + auto installedSource = installedVersion->GetSource(); + + if (installedSource.ContainsAvailablePackages()) + { + return installedSource.GetDetails().Priority; + } + } + } + + auto available = composite->GetAvailable(); + + if (!available.empty()) + { + return available.front()->GetSource().GetDetails().Priority; + } + + return std::nullopt; + } } diff --git a/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h b/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h index 0497646015..2a56d78d92 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h +++ b/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h @@ -38,4 +38,7 @@ namespace AppInstaller::Repository // Fills the options from the given metadata, optionally including the allowed architectures. void GetManifestComparatorOptionsFromMetadata(AppInstaller::Manifest::ManifestComparator::Options& options, const IPackageVersion::Metadata& metadata, bool includeAllowedArchitectures = true); + + // Gets the source priority for a given composite package, taking into account installed relationships. + std::optional GetSourcePriority(const std::shared_ptr& composite); } diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index d0177558a3..9b20f55302 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -155,6 +155,11 @@ namespace AppInstaller::Repository // Whether the source should be hidden by default unless explicitly declared. bool Explicit = false; + + // Value used for sorting the sources and making decisions + // (like preferring one source over the other if both have a package to install). + // Higher values come first in priority order. + int32_t Priority = 0; }; // Check if a source matches a well known source @@ -205,6 +210,9 @@ namespace AppInstaller::Repository // The Explicit property of a source. std::optional Explicit; + + // The Priority property of a source. + std::optional Priority; }; // Allows calling code to inquire about specific features of an ISource implementation. @@ -232,7 +240,7 @@ namespace AppInstaller::Repository Source(WellKnownSource source); // Constructor for a source to be added. - Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, bool isExplicit); + Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, const SourceEdit& additionalProperties); // Constructor for creating a composite source from a list of available sources. Source(const std::vector& availableSources); diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index 7450c16f09..5fdcccea0b 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -462,7 +462,7 @@ namespace AppInstaller::Repository m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); } - Source::Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, bool isExplicit) + Source::Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, const SourceEdit& additionalProperties) { m_isSourceToBeAdded = true; SourceDetails details; @@ -479,7 +479,14 @@ namespace AppInstaller::Repository details.Arg = arg; details.Type = type; details.TrustLevel = trustLevel; - details.Explicit = isExplicit; + if (additionalProperties.Explicit) + { + details.Explicit = additionalProperties.Explicit.value(); + } + if (additionalProperties.Priority) + { + details.Priority = additionalProperties.Priority.value(); + } } m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); @@ -1014,6 +1021,11 @@ namespace AppInstaller::Repository details.Explicit = edits.Explicit.value(); } + if (edits.Priority.has_value()) + { + details.Priority = edits.Priority.value(); + } + // Apply the edits and update source list. SourceList sourceList; sourceList.EditSource(details); @@ -1026,14 +1038,18 @@ namespace AppInstaller::Repository const auto& details = m_sourceReferences[0]->GetDetails(); - // For now the only supported editable difference is Explicit. - // If others are added, they would be checked below for changes. bool isChanged = false; + if (edits.Explicit.has_value() && edits.Explicit.value() != details.Explicit) { isChanged = true; } + if (edits.Priority.has_value() && edits.Priority.value() != details.Priority) + { + isChanged = true; + } + return isChanged; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp index 198c301905..d851ff6b28 100644 --- a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp +++ b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "RestSource.h" +#include "MatchCriteriaResolver.h" using namespace AppInstaller::Utility; @@ -509,10 +510,8 @@ namespace AppInstaller::Repository::Rest std::shared_ptr sharedThis = NonConstSharedFromThis(); for (auto& result : results.Matches) { - std::shared_ptr package = std::make_shared(sharedThis, std::move(result)); - - // TODO: Improve to use Package match filter to return relevant search results. - PackageMatchFilter packageFilter{ {}, {}, {} }; + std::shared_ptr package = std::make_shared(sharedThis, std::move(result)); + PackageMatchFilter packageFilter{ FindBestMatchCriteria(request, package->GetLatestVersion().get()) }; searchResult.Matches.emplace_back(std::move(package), std::move(packageFilter)); } diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp index 79194fc9e3..6cfd224764 100644 --- a/src/AppInstallerRepositoryCore/SourceList.cpp +++ b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -27,6 +27,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_SourcesYaml_Source_IsOverride = "IsOverride"sv; constexpr std::string_view s_SourcesYaml_Source_Explicit = "Explicit"sv; constexpr std::string_view s_SourcesYaml_Source_TrustLevel = "TrustLevel"sv; + constexpr std::string_view s_SourcesYaml_Source_Priority = "Priority"sv; constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; constexpr std::string_view s_MetadataYaml_Source_Name = "Name"sv; @@ -192,6 +193,7 @@ namespace AppInstaller::Repository out << YAML::Key << s_SourcesYaml_Source_IsOverride << YAML::Value << details.IsOverride; out << YAML::Key << s_SourcesYaml_Source_Explicit << YAML::Value << details.Explicit; out << YAML::Key << s_SourcesYaml_Source_TrustLevel << YAML::Value << static_cast(details.TrustLevel); + out << YAML::Key << s_SourcesYaml_Source_Priority << YAML::Value << details.Priority; out << YAML::EndMap; } } @@ -242,6 +244,13 @@ namespace AppInstaller::Repository { // These are the supported Override fields. Explicit = overrideSource.Explicit; + Priority = overrideSource.Priority; + } + + bool SourceDetailsInternal::operator<(const SourceDetailsInternal& other) const + { + // Higher values come first in ordering and must be "less than" for standard sorting + return Priority > other.Priority; } std::string_view GetWellKnownSourceName(WellKnownSource source) @@ -726,6 +735,11 @@ namespace AppInstaller::Repository } } } + + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SourcePriority)) + { + std::stable_sort(m_sourceList.begin(), m_sourceList.end()); + } } void SourceList::OverwriteMetadata() @@ -790,6 +804,7 @@ namespace AppInstaller::Repository TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Explicit, details.Explicit, false); TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier, false); TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsOverride, details.IsOverride, false); + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Priority, details.Priority, false); int64_t trustLevelValue; if (TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_TrustLevel, trustLevelValue, false)) diff --git a/src/AppInstallerRepositoryCore/SourceList.h b/src/AppInstallerRepositoryCore/SourceList.h index 5fb3052a3b..a2bd862169 100644 --- a/src/AppInstallerRepositoryCore/SourceList.h +++ b/src/AppInstallerRepositoryCore/SourceList.h @@ -28,6 +28,9 @@ namespace AppInstaller::Repository // Copies the overridden fields from the target source to this source. This is only the supported override fields. void CopyOverrideFieldsFrom(const SourceDetails& overrideSource); + // Sorts by Priority with higher values coming first in the order. + bool operator<(const SourceDetailsInternal& other) const; + // If true, this is a tombstone, marking the deletion of a source at a lower priority origin. bool IsTombstone = false; diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 99be199fda..fa0546cd58 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -201,6 +201,15 @@ namespace AppInstaller::Utility return (it != a.end()); } + bool ContainsSubstring(std::string_view a, std::string_view b) + { + auto it = std::search( + a.begin(), a.end(), + b.begin(), b.end() + ); + return (it != a.end()); + } + bool ICUCaseInsensitiveEquals(std::string_view a, std::string_view b) { return FoldCase(a) == FoldCase(b); @@ -994,6 +1003,20 @@ namespace AppInstaller::Utility } } + std::optional TryConvertStringToInt32(const std::string_view& input) + { + int32_t result = 0; + auto parseResult = std::from_chars(input.data(), input.data() + input.length(), result); + + std::optional optionalResult; + if (parseResult.ec == std::errc{}) + { + optionalResult = result; + } + + return optionalResult; + } + std::string ConvertGuidToString(const GUID& value) { wchar_t buffer[40]; diff --git a/src/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp index af57a4469c..cdaa4dceb0 100644 --- a/src/AppInstallerSharedLib/GroupPolicy.cpp +++ b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -219,7 +219,7 @@ namespace AppInstaller::Settings } } #endif - // TrustLevel and Explicit are optional policy fields with default values. + // TrustLevel, Explicit, and Priority are optional policy fields with default values. const std::string trustLevelName = "TrustLevel"; if (sourceJson.isMember(trustLevelName) && sourceJson[trustLevelName].isArray()) { @@ -236,6 +236,12 @@ namespace AppInstaller::Settings source.Explicit = sourceJson[explicitName].asBool(); } + const std::string priorityName = "Priority"; + if (sourceJson.isMember(priorityName) && sourceJson[priorityName].isInt()) + { + source.Priority = sourceJson[priorityName].asInt(); + } + return source; } } @@ -360,6 +366,11 @@ namespace AppInstaller::Settings json["Identifier"] = Identifier; json["Explicit"] = Explicit; + if (Priority) + { + json["Priority"] = Priority.value(); + } + // Trust level is represented as an array of trust level strings since there can be multiple flags set. int trustLevelLength = static_cast(TrustLevel.size()); for (int i = 0; i < trustLevelLength; ++i) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index ea2e1af88a..e0e32c9c62 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -131,6 +131,9 @@ namespace AppInstaller::Utility // Use this if one of the values is a known value, and thus ToLower is sufficient. bool CaseInsensitiveContainsSubstring(std::string_view a, std::string_view b); + // Determines if string a contains string b. + bool ContainsSubstring(std::string_view a, std::string_view b); + // Compares the two UTF8 strings in a case-insensitive manner, using ICU for case folding. bool ICUCaseInsensitiveEquals(std::string_view a, std::string_view b); @@ -301,6 +304,9 @@ namespace AppInstaller::Utility // Converts the given string view into a bool. std::optional TryConvertStringToBool(const std::string_view& value); + // Converts the given string view into an int32. + std::optional TryConvertStringToInt32(const std::string_view& value); + // Converts the given GUID value to a string. std::string ConvertGuidToString(const GUID& value); diff --git a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h index 08e8063a03..6180e53948 100644 --- a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h +++ b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h @@ -88,6 +88,7 @@ namespace AppInstaller::Settings std::string Identifier; std::vector TrustLevel; bool Explicit = false; + std::optional Priority; #ifndef AICLI_DISABLE_TEST_HOOKS Certificates::PinningConfiguration PinningConfiguration; diff --git a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.cpp b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.cpp index b31d0962f5..d06a9c366a 100644 --- a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.cpp +++ b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.cpp @@ -58,9 +58,20 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return m_explicit; } - void AddPackageCatalogOptions::Explicit(bool const& value) + void AddPackageCatalogOptions::Explicit(bool value) { m_explicit = value; } + + int32_t AddPackageCatalogOptions::Priority() + { + return m_priority; + } + + void AddPackageCatalogOptions::Priority(int32_t value) + { + m_priority = value; + } + CoCreatableMicrosoftManagementDeploymentClass(AddPackageCatalogOptions); } diff --git a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h index 6d2c487c8c..b9ac230748 100644 --- a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h @@ -28,7 +28,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation void CustomHeader(hstring const& value); bool Explicit(); - void Explicit(bool const& value); + void Explicit(bool value); + + int32_t Priority(); + void Priority(int32_t value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -38,6 +41,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogTrustLevel m_trustLevel = winrt::Microsoft::Management::Deployment::PackageCatalogTrustLevel::None; hstring m_customHeader = L""; bool m_explicit = false; + int32_t m_priority = 0; #endif }; } diff --git a/src/Microsoft.Management.Deployment/CatalogPackage.cpp b/src/Microsoft.Management.Deployment/CatalogPackage.cpp index e83eaa9975..f816bd67d9 100644 --- a/src/Microsoft.Management.Deployment/CatalogPackage.cpp +++ b/src/Microsoft.Management.Deployment/CatalogPackage.cpp @@ -176,4 +176,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return m_package; } + + Windows::Foundation::IReference CatalogPackage::CatalogPriority() + { + return AppInstaller::Repository::GetSourcePriority(m_package); + } } diff --git a/src/Microsoft.Management.Deployment/CatalogPackage.h b/src/Microsoft.Management.Deployment/CatalogPackage.h index 5bee838d06..78b65b2c45 100644 --- a/src/Microsoft.Management.Deployment/CatalogPackage.h +++ b/src/Microsoft.Management.Deployment/CatalogPackage.h @@ -1,49 +1,51 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "CatalogPackage.g.h" - -namespace winrt::Microsoft::Management::Deployment::implementation -{ - struct CatalogPackage : CatalogPackageT - { - CatalogPackage() = default; - -#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) - void Initialize( - ::AppInstaller::Repository::Source source, - std::shared_ptr<::AppInstaller::Repository::ICompositePackage> package); - std::shared_ptr<::AppInstaller::Repository::ICompositePackage> GetRepositoryPackage(); -#endif - - hstring Id(); - hstring Name(); - winrt::Microsoft::Management::Deployment::PackageVersionInfo InstalledVersion(); - winrt::Windows::Foundation::Collections::IVectorView AvailableVersions(); - winrt::Microsoft::Management::Deployment::PackageVersionInfo DefaultInstallVersion(); - winrt::Microsoft::Management::Deployment::PackageVersionInfo GetPackageVersionInfo(winrt::Microsoft::Management::Deployment::PackageVersionId const& versionKey); - bool IsUpdateAvailable(); - // Contract 5.0 - winrt::Windows::Foundation::IAsyncOperation CheckInstalledStatusAsync( - winrt::Microsoft::Management::Deployment::InstalledStatusType checkTypes); - winrt::Microsoft::Management::Deployment::CheckInstalledStatusResult CheckInstalledStatus( - winrt::Microsoft::Management::Deployment::InstalledStatusType checkTypes); - winrt::Windows::Foundation::IAsyncOperation CheckInstalledStatusAsync(); - winrt::Microsoft::Management::Deployment::CheckInstalledStatusResult CheckInstalledStatus(); - -#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) - private: - ::AppInstaller::Repository::Source m_source; - std::shared_ptr<::AppInstaller::Repository::ICompositePackage> m_package; - bool m_updateAvailable = false; - Windows::Foundation::Collections::IVector m_availableVersions{ winrt::single_threaded_vector() }; - winrt::Microsoft::Management::Deployment::PackageVersionInfo m_installedVersion{ nullptr }; - winrt::Microsoft::Management::Deployment::PackageVersionInfo m_latestApplicableVersion{ nullptr }; - std::once_flag m_installedVersionOnceFlag; - std::once_flag m_availableVersionsOnceFlag; - std::once_flag m_latestApplicableVersionOnceFlag; - - void InitializeLatestApplicableVersion(); -#endif - }; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "CatalogPackage.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct CatalogPackage : CatalogPackageT + { + CatalogPackage() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + ::AppInstaller::Repository::Source source, + std::shared_ptr<::AppInstaller::Repository::ICompositePackage> package); + std::shared_ptr<::AppInstaller::Repository::ICompositePackage> GetRepositoryPackage(); +#endif + + hstring Id(); + hstring Name(); + winrt::Microsoft::Management::Deployment::PackageVersionInfo InstalledVersion(); + winrt::Windows::Foundation::Collections::IVectorView AvailableVersions(); + winrt::Microsoft::Management::Deployment::PackageVersionInfo DefaultInstallVersion(); + winrt::Microsoft::Management::Deployment::PackageVersionInfo GetPackageVersionInfo(winrt::Microsoft::Management::Deployment::PackageVersionId const& versionKey); + bool IsUpdateAvailable(); + // Contract 5.0 + winrt::Windows::Foundation::IAsyncOperation CheckInstalledStatusAsync( + winrt::Microsoft::Management::Deployment::InstalledStatusType checkTypes); + winrt::Microsoft::Management::Deployment::CheckInstalledStatusResult CheckInstalledStatus( + winrt::Microsoft::Management::Deployment::InstalledStatusType checkTypes); + winrt::Windows::Foundation::IAsyncOperation CheckInstalledStatusAsync(); + winrt::Microsoft::Management::Deployment::CheckInstalledStatusResult CheckInstalledStatus(); + + Windows::Foundation::IReference CatalogPriority(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + ::AppInstaller::Repository::Source m_source; + std::shared_ptr<::AppInstaller::Repository::ICompositePackage> m_package; + bool m_updateAvailable = false; + Windows::Foundation::Collections::IVector m_availableVersions{ winrt::single_threaded_vector() }; + winrt::Microsoft::Management::Deployment::PackageVersionInfo m_installedVersion{ nullptr }; + winrt::Microsoft::Management::Deployment::PackageVersionInfo m_latestApplicableVersion{ nullptr }; + std::once_flag m_installedVersionOnceFlag; + std::once_flag m_availableVersionsOnceFlag; + std::once_flag m_latestApplicableVersionOnceFlag; + + void InitializeLatestApplicableVersion(); +#endif + }; } diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp index 589893aef3..5782ad0dd5 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp @@ -17,22 +17,32 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring EditPackageCatalogOptions::Name() { return hstring(m_name); - } + } void EditPackageCatalogOptions::Name(hstring const& value) { m_name = value; - } + } Windows::Foundation::IReference EditPackageCatalogOptions::Explicit() { return m_explicit; - } + } void EditPackageCatalogOptions::Explicit(Windows::Foundation::IReference value) { m_explicit = value; } + Windows::Foundation::IReference EditPackageCatalogOptions::Priority() + { + return m_priority; + } + + void EditPackageCatalogOptions::Priority(Windows::Foundation::IReference value) + { + m_priority = value; + } + CoCreatableMicrosoftManagementDeploymentClass(EditPackageCatalogOptions); } diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h index 754ab3a370..28e3703e0d 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h @@ -3,7 +3,8 @@ #pragma once #include "EditPackageCatalogOptions.g.h" #include "public/ComClsids.h" -#include +#include +#include #include namespace winrt::Microsoft::Management::Deployment::implementation @@ -19,10 +20,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation Windows::Foundation::IReference Explicit(); void Explicit(Windows::Foundation::IReference value); + Windows::Foundation::IReference Priority(); + void Priority(Windows::Foundation::IReference value); + #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: hstring m_name = L""; std::optional m_explicit; + std::optional m_priority; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp b/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp index 64d9df05a6..65d938bc13 100644 --- a/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp +++ b/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp @@ -60,5 +60,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool PackageCatalogInfo::Explicit() { return m_sourceDetails.Explicit; + } + + int32_t PackageCatalogInfo::Priority() + { + return m_sourceDetails.Priority; } } diff --git a/src/Microsoft.Management.Deployment/PackageCatalogInfo.h b/src/Microsoft.Management.Deployment/PackageCatalogInfo.h index efee699c3a..68913f6ea8 100644 --- a/src/Microsoft.Management.Deployment/PackageCatalogInfo.h +++ b/src/Microsoft.Management.Deployment/PackageCatalogInfo.h @@ -23,6 +23,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogOrigin Origin(); winrt::Microsoft::Management::Deployment::PackageCatalogTrustLevel TrustLevel(); bool Explicit(); + int32_t Priority(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index ae991b1dea..39d53771b5 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -108,7 +108,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation CheckForDuplicateSource(name, type, sourceUri); - ::AppInstaller::Repository::Source source = ::AppInstaller::Repository::Source{ name, sourceUri, type, trustLevel, options.Explicit() }; + ::AppInstaller::Repository::SourceEdit additionalProperties; + additionalProperties.Explicit = options.Explicit(); + additionalProperties.Priority = options.Priority(); + + ::AppInstaller::Repository::Source source = ::AppInstaller::Repository::Source{ name, sourceUri, type, trustLevel, additionalProperties }; std::string customHeader = winrt::to_string(options.CustomHeader()); if (!customHeader.empty()) @@ -1469,6 +1473,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation ::AppInstaller::Repository::SourceEdit edits; edits.Explicit = options.Explicit(); + edits.Priority = options.Priority(); + if (sourceToEdit.RequiresChanges(edits)) { sourceToEdit.Edit(edits); diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index c2cc718609..25567032f2 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(28)] // For version 1.28 + [contractversion(29)] // For version 1.29 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -335,6 +335,12 @@ namespace Microsoft.Management.Deployment /// Excludes a source from discovery unless specified. Boolean Explicit{ get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// The priority of this catalog. Higher values are sorted first. + Int32 Priority{ get; }; + } } /// A metadata item of a package version. @@ -632,10 +638,13 @@ namespace Microsoft.Management.Deployment CheckInstalledStatusResult CheckInstalledStatus(); } - /// DESIGN NOTE: - /// IsSame from IPackage in winget/RepositorySearch is not implemented in V1. - /// Determines if the given IPackage refers to the same package as this one. - /// virtual bool IsSame(const IPackage*) const = 0; + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// Determines the priority of the catalog for this package object. + /// This should match the priority of the DefaultInstallVersion, but it is much more efficient than using that route. + /// May be null if the package refers only to an installed item. + Windows.Foundation.IReference CatalogPriority { get; }; + } } /// IMPLEMENTATION NOTE: CompositeSearchBehavior from winget/RepositorySource.h @@ -1473,6 +1482,12 @@ namespace Microsoft.Management.Deployment /// Excludes a source from discovery unless specified. Boolean Explicit; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// The priority of this catalog. Higher values are sorted first. + Int32 Priority; + } }; /// IMPLEMENTATION NOTE: AddPackageCatalogStatus @@ -1552,8 +1567,14 @@ namespace Microsoft.Management.Deployment /// For contoso sample on msdn "contoso" String Name; - /// Editing the Explicit property has three states: true, false, and not specified (no changes). + /// Editing the Explicit property has three states: true, false, and not specified (null). Windows.Foundation.IReference Explicit; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// The priority of this catalog. Higher values are sorted first. + Windows.Foundation.IReference Priority; + } }; /// IMPLEMENTATION NOTE: RemovePackageCatalogStatus diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs index b9b6a4f418..aa9cf2a0a4 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs @@ -42,9 +42,16 @@ public sealed class AddSourceCmdlet : PSCmdlet [Parameter( ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] +#if AICLI_DISABLE_TEST_HOOKS [ValidateSet( "Microsoft.Rest", "Microsoft.PreIndexed.Package")] +#else + [ValidateSet( + "Microsoft.Rest", + "Microsoft.PreIndexed.Package", + "Microsoft.Test.Configurable")] +#endif public string Type { get; set; } /// @@ -60,13 +67,20 @@ public sealed class AddSourceCmdlet : PSCmdlet [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter Explicit { get; set; } + /// + /// Gets or sets a value indicating the priority of the source. Higher values are sorted first. + /// + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public int Priority { get; set; } + /// /// Adds source. /// protected override void ProcessRecord() { var command = new CliCommand(this); - command.AddSource(this.Name, this.Argument, this.Type, this.ConvertPSSourceTrustLevelToString(this.TrustLevel), this.Explicit.ToBool()); + command.AddSource(this.Name, this.Argument, this.Type, this.ConvertPSSourceTrustLevelToString(this.TrustLevel), this.Explicit.ToBool(), this.Priority); } private string ConvertPSSourceTrustLevelToString(PSSourceTrustLevel trustLevel) => trustLevel switch diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs index 25b9b9ba47..270e4e0e5f 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs @@ -72,7 +72,8 @@ public void GetSettings(bool asPlainText) /// Type of source. /// Trust level of source. /// Make source explicit. - public void AddSource(string name, string arg, string type, string trustLevel, bool isExplicit) + /// Set the priority if the source. + public void AddSource(string name, string arg, string type, string trustLevel, bool isExplicit, int priority) { Utilities.VerifyAdmin(); var builder = new WinGetCLICommandBuilder("source") @@ -95,6 +96,11 @@ public void AddSource(string name, string arg, string type, string trustLevel, b builder.AppendSwitch("explicit"); } + if (priority != 0) + { + builder.AppendOption("priority", priority.ToString()); + } + _ = this.Run(builder, 300000); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs index bc1b7a864b..62f8c6ce22 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs @@ -116,6 +116,45 @@ private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior, Packa } else { + if (behavior != CompositeSearchBehavior.LocalCatalogs) + { + List highestPriorityResults = new List(); + int? highestPriority = null; + + for (int i = 0; i < results.Count; i++) + { + MatchResult result = results[i]; + int? priority = result.CatalogPackage.CatalogPriority; + + if ((highestPriority == null && priority != null) || highestPriority < priority) + { + // Current priority is higher; reset. + highestPriority = priority; + highestPriorityResults.Clear(); + } + else if (highestPriority == priority) + { + // Priority is equal, add to the list. + } + else + { + // Current priority is lower, ignore the match. + continue; + } + + highestPriorityResults.Add(result); + } + + if (highestPriorityResults.Count == 1) + { + return highestPriorityResults[0].CatalogPackage; + } + else + { + throw new VagueCriteriaException(highestPriorityResults); + } + } + // Too many packages matched! The user needs to refine their input. throw new VagueCriteriaException(results); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs index bdea04a132..75a5f282f8 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs @@ -23,6 +23,7 @@ internal PSSourceResult(Management.Deployment.PackageCatalogReference catalogRef this.Type = info.Type; this.TrustLevel = info.TrustLevel.ToString(); this.Explicit = info.Explicit; + this.Priority = info.Priority; } /// @@ -49,5 +50,10 @@ internal PSSourceResult(Management.Deployment.PackageCatalogReference catalogRef /// Gets a value indicating whether the source must be explicitly specified for discovery. /// public bool Explicit { get; private set; } + + /// + /// Gets a value indicating the priority of the source. Higher values are sorted first. + /// + public int Priority { get; private set; } } } diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 index 96938c707a..cd4650262e 100644 --- a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -203,6 +203,9 @@ class WinGetSource [DscProperty()] [nullable[bool]]$Explicit = $null + + [DscProperty()] + [nullable[int]]$Priority = $null [DscProperty()] [WinGetEnsure]$Ensure = [WinGetEnsure]::Present @@ -232,6 +235,7 @@ class WinGetSource $result.Type = $currentSource.Type $result.TrustLevel = $currentSource.TrustLevel $result.Explicit = $currentSource.Explicit + $result.Priority = $currentSource.Priority } else { @@ -314,6 +318,11 @@ class WinGetSource $hashArgs.Add("Explicit", $this.Explicit) } + if ($null -ne $this.Priority) + { + $hashArgs.Add("Priority", $this.Priority) + } + Add-WinGetSource @hashArgs } } @@ -352,6 +361,12 @@ class WinGetSource return $false } + if ($null -ne $this.Priority -and + $this.Priority -ne $currentSource.Priority) + { + return $false + } + return $true } } diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index bf133aa0d1..f8042c2565 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -188,7 +188,10 @@ Describe 'Reset-WinGetSource' { Describe 'Get|Add|Reset-WinGetSource' { BeforeAll { - Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -TrustLevel 'Trusted' -Explicit + $ogSettings = @{ experimentalFeatures= @{sourcePriority=$true}} + SetWinGetSettingsHelper $ogSettings + + Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -TrustLevel 'Trusted' -Explicit -Priority 42 } It 'Get Test source' { @@ -200,6 +203,7 @@ Describe 'Get|Add|Reset-WinGetSource' { $source.Type | Should -Be 'Microsoft.PreIndexed.Package' $source.TrustLevel | Should -Be 'Trusted' $source.Explicit | Should -Be $true + $source.Priority | Should -Be 42 } It 'Get fake source' { @@ -210,6 +214,11 @@ Describe 'Get|Add|Reset-WinGetSource' { It 'Reset Test source' { Reset-WinGetSource -Name TestSource } + + AfterAll { + RemoveTestSource + RestoreWinGetSettings + } } Describe 'Find-WinGetPackage' { @@ -493,6 +502,50 @@ Describe 'Install|Repair|Uninstall-WinGetPackage' { } } +Describe 'Install-WinGetPackage Source Priority' { + + It 'Install equal Priority' { + AddTestSource + Add-WinGetSource -Name 'dummyPackageSource' -Type 'Microsoft.Test.Configurable' -Arg '{"ContainsPackage":true}' + + { Install-WinGetPackage -Id AppInstallerTest.TestExeInstaller -Version '1.0.0.0' } | Should -Throw + } + + It 'Install higher Priority' { + $ogSettings = @{ experimentalFeatures= @{sourcePriority=$true}} + SetWinGetSettingsHelper $ogSettings + + RemoveTestSource + Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -Priority 1 + Add-WinGetSource -Name 'dummyPackageSource' -Type 'Microsoft.Test.Configurable' -Arg '{"ContainsPackage":true}' + + $expectedExeInstallerResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestExeInstaller" + Name = "TestExeInstaller" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + UninstallerErrorCode = 0 + } + + $result = Install-WinGetPackage -Id AppInstallerTest.TestExeInstaller -Version '1.0.0.0' + Validate-WinGetPackageOperationResult $result $expectedExeInstallerResult 'install' + } + + AfterEach { + $testExe = Get-WinGetPackage -Id AppInstallerTest.TestExeInstaller -MatchOption Equals + if ($testExe.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.TestExeInstaller + } + + Remove-WinGetSource -Name 'dummyPackageSource' + RemoveTestSource + RestoreWinGetSettings + } +} + Describe 'Get-WinGetPackage' { BeforeAll { diff --git a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 index 7be3009dd8..627bb8ebb5 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 @@ -130,6 +130,8 @@ Describe 'WinGetUserSettings' { Describe 'WinGetSource' { BeforeAll { + InvokeWinGetDSC -Name WinGetUserSettings -Method Set -Property @{ Settings = @{ experimentalFeatures = @{ sourcePriority = $true } } } + $testSourceName = 'TestSource' $testSourceArg = 'https://localhost:5001/TestKit/' $testSourceType = 'Microsoft.PreIndexed.Package' @@ -148,7 +150,7 @@ Describe 'WinGetSource' { } It 'Set WinGet source' { - InvokeWinGetDSC -Name WinGetSource -Method Set -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType; TrustLevel = 'Trusted'; Explicit = $true } + InvokeWinGetDSC -Name WinGetSource -Method Set -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType; TrustLevel = 'Trusted'; Explicit = $true; Priority = 42 } $result = InvokeWinGetDSC -Name WinGetSource -Method Test -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType } $result.InDesiredState | Should -Be $true @@ -159,6 +161,7 @@ Describe 'WinGetSource' { $result.Argument | Should -Be $testSourceArg $result.TrustLevel | Should -Be 'Trusted' $result.Explicit | Should -Be $true + $result.Priority | Should -Be 42 } }