From ed4322efecd456dbf9d9a002376f4fe6cc8ad78f Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 23 Jan 2026 14:41:54 -0800 Subject: [PATCH 01/15] Baseline source priority property in CLI --- doc/Settings.md | 10 ++++ .../JSON/settings/settings.schema.0.2.json | 5 ++ src/AppInstallerCLICore/Argument.cpp | 6 ++- .../Commands/SourceCommand.cpp | 22 ++++++++ src/AppInstallerCLICore/ExecutionArgs.h | 1 + src/AppInstallerCLICore/Resources.h | 2 + .../Workflows/SourceFlow.cpp | 53 ++++++++++++++++--- .../Helpers/WinGetSettingsHelper.cs | 2 + src/AppInstallerCLIE2ETests/SourceCommand.cs | 49 ++++++++++++++++- .../Shared/Strings/en-us/winget.resw | 8 +++ src/AppInstallerCLITests/Sources.cpp | 35 +++++++++--- src/AppInstallerCLITests/TestHooks.h | 34 ++++++++++++ src/AppInstallerCLITests/TestSettings.cpp | 2 +- src/AppInstallerCLITests/TestSettings.h | 1 + src/AppInstallerCLITests/TestSource.cpp | 2 +- .../ExperimentalFeature.cpp | 25 +++++++++ .../Public/winget/ExperimentalFeature.h | 1 + .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + .../Public/winget/RepositorySource.h | 14 +++-- .../RepositorySource.cpp | 26 ++++++--- src/AppInstallerRepositoryCore/SourceList.cpp | 15 ++++++ src/AppInstallerRepositoryCore/SourceList.h | 3 ++ .../AppInstallerStrings.cpp | 14 +++++ src/AppInstallerSharedLib/GroupPolicy.cpp | 5 ++ .../Public/AppInstallerStrings.h | 3 ++ .../Public/winget/GroupPolicy.h | 1 + .../PackageManager.cpp | 5 +- 28 files changed, 319 insertions(+), 28 deletions(-) diff --git a/doc/Settings.md b/doc/Settings.md index 2beeced86c..843f9478d1 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -430,3 +430,13 @@ To enable: "listDetails": true }, ``` + +### sourcePriority + +This feature enables sources to have a priority value assigned. Sources with a higher priority will appear first in search results and will be selected for installing new packages when multiple sources have a matching package. + +```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 c8eb465cbf..c8558843ee 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -338,6 +338,11 @@ "description": "Enable source edit command", "type": "boolean", "default": false + }, + "sourcePriority": { + "description": "Enable source priority feature", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 12289a3888..47cdb54ba3 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/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp index 968873b757..5b90e7149d 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..93e97c72b0 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -722,6 +722,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 +732,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/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/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index dea4021ef5..b7e9638835 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -227,6 +227,8 @@ public static void InitializeAllFeatures(bool status) ConfigureFeature(settingsJson, "resume", status); ConfigureFeature(settingsJson, "reboot", status); ConfigureFeature(settingsJson, "fonts", status); + ConfigureFeature(settingsJson, "sourceEdit", status); + ConfigureFeature(settingsJson, "sourcePriority", status); SetWingetSettings(settingsJson); } diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index 4f60b94455..297fc9bd21 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -21,6 +21,7 @@ public class SourceCommand : BaseCommand public void OneTimeSetup() { WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); + WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } /// @@ -106,6 +107,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", ""); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); + Assert.True(exportResult.StdOut.Contains("42")); + } + /// /// Test source add with duplicate name. /// @@ -269,7 +292,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 +327,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..cd9b093963 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3535,4 +3535,12 @@ An unlocalized JSON fragment will follow on another line. New Value Column title for listing the new value. + + Priority with 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. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index 7c0a31dfeb..8f524121fa 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)); @@ -383,7 +391,20 @@ TEST_CASE("RepoSources_ThreeSources", "[sources]") std::vector sources = GetSources(); REQUIRE(sources.size() == 3); - const char* suffix[3] = { "", "2", "3" }; + const char* suffixUnsorted[3] = { "", "2", "3" }; + const char* suffixPrioritySorted[3] = { "2", "3", "" }; + const char** suffix = nullptr; + std::unique_ptr override; + + SECTION("Unsorted") + { + suffix = suffixUnsorted; + } + SECTION("Priority Sorted") + { + override = std::make_unique(ExperimentalFeature::Feature::SourcePriority); + suffix = suffixPrioritySorted; + } for (size_t i = 0; i < 3; ++i) { @@ -423,6 +444,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 +467,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 +1363,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 +1374,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..ef5e4ce828 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -462,7 +462,7 @@ 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::Source source{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, {} }; return source.Add(progress); } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index c38f37e4de..27a7ef351b 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, << @@ -48,6 +69,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::SourceEdit: return userSettings.Get(); + case ExperimentalFeature::Feature::SourcePriority: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -85,6 +108,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "List Details", "listDetails", "https://aka.ms/winget-settings", Feature::ListDetails }; case Feature::SourceEdit: return ExperimentalFeature{ "Source Editing", "sourceEdit", "https://aka.ms/winget-settings", Feature::SourceEdit }; + 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 70cd889d63..e8f85bce06 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,6 +27,7 @@ namespace AppInstaller::Settings Font = 0x4, ListDetails = 0x8, SourceEdit = 0x10, + SourcePriority = 0x20, 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 bf3c03f8cd..f105c72b4b 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -78,6 +78,7 @@ namespace AppInstaller::Settings EFFonts, EFListDetails, EFSourceEdit, + EFSourcePriority, // Telemetry TelemetryDisable, // Install behavior @@ -167,6 +168,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFListDetails, bool, bool, false, ".experimentalFeatures.listDetails"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFSourceEdit, bool, bool, false, ".experimentalFeatures.sourceEdit"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 d5147076ae..2f2114ba20 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -269,6 +269,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFFonts) WINGET_VALIDATE_PASS_THROUGH(EFListDetails) WINGET_VALIDATE_PASS_THROUGH(EFSourceEdit) + 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/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index 78e726bd93..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 @@ -201,10 +206,13 @@ namespace AppInstaller::Repository // Contains information about edits to a source. struct SourceEdit { - SourceEdit(std::optional isExplicit); + SourceEdit() = default; - // The explicit property of a source. + // 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 35209e28c2..5fdcccea0b 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -432,8 +432,6 @@ namespace AppInstaller::Repository return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type); } - SourceEdit::SourceEdit(std::optional isExplicit) : Explicit(isExplicit) {} - Source::Source() {} Source::Source(std::string_view name) @@ -464,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; @@ -481,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)); @@ -1016,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); @@ -1028,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/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..c895973e02 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -994,6 +994,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..bf2c8fe017 100644 --- a/src/AppInstallerSharedLib/GroupPolicy.cpp +++ b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -360,6 +360,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..929db972a4 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -301,6 +301,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/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index fd08134c93..6848207047 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -108,7 +108,10 @@ 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(); + + ::AppInstaller::Repository::Source source = ::AppInstaller::Repository::Source{ name, sourceUri, type, trustLevel, additionalProperties }; std::string customHeader = winrt::to_string(options.CustomHeader()); if (!customHeader.empty()) From 89a814232a1faf782044a6f0a142469e8d5e1732 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 23 Jan 2026 16:37:50 -0800 Subject: [PATCH 02/15] Add to dscv3 command and tests --- .../Commands/DscComposableObject.h | 14 +++ .../Commands/DscSourceResource.cpp | 97 ++++++++++++++++++- src/AppInstallerCLICore/Resources.h | 1 + .../DSCv3SourceResourceCommand.cs | 90 ++++++++++++++--- .../Shared/Strings/en-us/winget.resw | 3 + 5 files changed, 185 insertions(+), 20 deletions(-) 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..87a24e475c 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,51 @@ 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() && + Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourceEdit)) + { + // 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 +240,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 +289,11 @@ namespace AppInstaller::CLI { result.append(std::string{ ExplicitProperty::Name() }); } + + if (!TestPriority()) + { + result.append(std::string{ PriorityProperty::Name() }); + } } return result; @@ -308,6 +369,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 +499,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/Resources.h b/src/AppInstallerCLICore/Resources.h index 93e97c72b0..1414941032 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); diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs index ad48c68d50..09e4e6f884 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,8 @@ public void OneTimeTeardown() public void Setup() { RemoveTestSource(); + WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); + WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } /// @@ -155,19 +159,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 +192,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 +216,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 +241,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 +265,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 +275,7 @@ public void Source_Test_AllMatch() Type = DefaultSourceType, TrustLevel = TrustedTrustLevel, Explicit = true, + Priority = 42, }; var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); @@ -383,6 +398,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 +460,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,10 +482,10 @@ 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); @@ -449,6 +502,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 +535,8 @@ private class SourceResourceData public bool? Explicit { get; set; } public bool? AcceptAgreements { get; set; } + + public int? Priority { get; set; } } } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index cd9b093963..5dbed873f2 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3543,4 +3543,7 @@ An unlocalized JSON fragment will follow on another line. 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. + \ No newline at end of file From 08f115e6a36c556d658b033edc5f9770f0a5776e Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 26 Jan 2026 10:44:34 -0800 Subject: [PATCH 03/15] config export and COM --- src/AppInstallerCLI.sln | 11 ++++- .../Workflows/ConfigurationFlow.cpp | 39 ++++++++++-------- .../Interop/PackageCatalogInterop.cs | 16 ++++++-- src/AppInstallerCLIE2ETests/SourceCommand.cs | 2 +- .../AddPackageCatalogOptions.cpp | 13 +++++- .../AddPackageCatalogOptions.h | 6 ++- .../Converters.cpp | 33 --------------- .../Converters.h | 2 - .../EditPackageCatalogOptions.cpp | 16 +++++++- .../EditPackageCatalogOptions.h | 12 ++++-- .../PackageCatalogInfo.cpp | 5 +++ .../PackageCatalogInfo.h | 1 + .../PackageManager.cpp | 6 ++- .../PackageManager.idl | 40 +++++++++++-------- 14 files changed, 120 insertions(+), 82 deletions(-) diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 58974840c1..2c80f694fe 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36811.4 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "AppInstallerCLIPackage", "AppInstallerCLIPackage\AppInstallerCLIPackage.wapproj", "{6AA3791A-0713-4548-A357-87A323E7AC3A}" ProjectSection(ProjectDependencies) = postProject @@ -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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1080,6 +1086,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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} 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/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs index 5493f951ae..b312e4840d 100644 --- a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs @@ -291,13 +291,16 @@ public async Task AddEditRemovePackageCatalog() options.SourceUri = Constants.TestSourceUrl; options.Name = Constants.TestSourceName; options.TrustLevel = PackageCatalogTrustLevel.Trusted; + options.Explicit = true; + options.Priority = 12; await this.AddAndValidatePackageCatalogAsync(options, AddPackageCatalogStatus.Ok); // Edit EditPackageCatalogOptions editOptions = this.TestFactory.CreateEditPackageCatalogOptions(); editOptions.Name = Constants.TestSourceName; - editOptions.Explicit = OptionalBoolean.False; + editOptions.Explicit = false; + editOptions.Priority = 42; this.EditAndValidatePackageCatalog(editOptions, EditPackageCatalogStatus.Ok); // Remove @@ -340,6 +343,8 @@ private PackageCatalogReference GetAndValidatePackageCatalog(AddPackageCatalogOp Assert.IsNotNull(packageCatalog); 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; } @@ -396,9 +401,14 @@ private void EditAndValidatePackageCatalog(EditPackageCatalogOptions editPackage // Verify edits are correct. var packageCatalog = this.packageManager.GetPackageCatalogByName(editPackageCatalogOptions.Name); - if (editPackageCatalogOptions.Explicit != OptionalBoolean.Unspecified) + if (editPackageCatalogOptions.Explicit != null) { - Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit == OptionalBoolean.True); + 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 297fc9bd21..46f4eb4ab1 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -124,7 +124,7 @@ public void SourceAddWithPriority() Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); Assert.True(listResult.StdOut.Contains("42")); - var exportResult = TestCommon.RunAICLICommand("source export", ""); + var exportResult = TestCommon.RunAICLICommand("source export", string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); Assert.True(exportResult.StdOut.Contains("42")); } 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/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 80def812a2..3c3b4f238b 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -556,37 +556,4 @@ namespace winrt::Microsoft::Management::Deployment::implementation default: return AppInstaller::Manifest::PlatformEnum::Unknown; } } - - std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean) - { - switch (optionalBoolean) - { - case OptionalBoolean::True: - return std::optional { true }; - case OptionalBoolean::False: - return std::optional { false }; - default: - return std::nullopt; - } - } - - winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean) - { - if (optionalBoolean.has_value()) - { - if (optionalBoolean.value()) - { - return OptionalBoolean::True; - } - else - { - return OptionalBoolean::False; - } - } - else - { - return OptionalBoolean::Unspecified; - } - } - } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index f2535704ab..460fe4e21c 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -35,8 +35,6 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); - std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean); - winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ if constexpr (std::is_same_v) \ diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp index 906106fa54..5d431b4c50 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp @@ -22,14 +22,26 @@ namespace winrt::Microsoft::Management::Deployment::implementation { m_name = value; } - OptionalBoolean EditPackageCatalogOptions::Explicit() + + Windows::Foundation::IReference EditPackageCatalogOptions::Explicit() { return m_explicit; } - void EditPackageCatalogOptions::Explicit(OptionalBoolean const& value) + + 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 68e3372f0d..28e3703e0d 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h @@ -4,6 +4,8 @@ #include "EditPackageCatalogOptions.g.h" #include "public/ComClsids.h" #include +#include +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -15,13 +17,17 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring Name(); void Name(hstring const& value); - OptionalBoolean Explicit(); - void Explicit(OptionalBoolean const& value); + 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""; - OptionalBoolean m_explicit = OptionalBoolean::Unspecified; + 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 6848207047..39d53771b5 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -110,6 +110,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation ::AppInstaller::Repository::SourceEdit additionalProperties; additionalProperties.Explicit = options.Explicit(); + additionalProperties.Priority = options.Priority(); ::AppInstaller::Repository::Source source = ::AppInstaller::Repository::Source{ name, sourceUri, type, trustLevel, additionalProperties }; @@ -1470,7 +1471,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, !matchingSource.has_value()); ::AppInstaller::Repository::Source sourceToEdit = ::AppInstaller::Repository::Source{ matchingSource.value().Name }; - ::AppInstaller::Repository::SourceEdit edits{ GetOptionalBoolean(options.Explicit())}; + ::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 da4976c3ac..e82b4eeb0c 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -335,6 +335,12 @@ namespace Microsoft.Management.Deployment /// Excludes a source from discovery unless specified. Boolean Explicit{ get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + /// The priority of this catalog. Higher values are sorted first. + Int32 Priority{ get; }; + } } /// A metadata item of a package version. @@ -1473,6 +1479,12 @@ namespace Microsoft.Management.Deployment /// Excludes a source from discovery unless specified. Boolean Explicit; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + /// The priority of this catalog. Higher values are sorted first. + Int32 Priority; + } }; /// IMPLEMENTATION NOTE: AddPackageCatalogStatus @@ -1541,15 +1553,6 @@ namespace Microsoft.Management.Deployment HRESULT ExtendedErrorCode { get; }; }; - /// IMPLEMENTATION NOTE: OptionalBoolean - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] - enum OptionalBoolean - { - Unspecified, - False, - True, - }; - /// IMPLEMENTATION NOTE: EditPackageCatalogOptions [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] runtimeclass EditPackageCatalogOptions @@ -1561,8 +1564,11 @@ 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). - OptionalBoolean Explicit; + /// Editing the Explicit property has three states: true, false, and not specified (null). + Windows.Foundation.IReference Explicit; + + /// The priority of this catalog. Higher values are sorted first. + Windows.Foundation.IReference Priority; }; /// IMPLEMENTATION NOTE: RemovePackageCatalogStatus @@ -1620,12 +1626,6 @@ namespace Microsoft.Management.Deployment Windows.Foundation.IAsyncOperationWithProgress RemovePackageCatalogAsync(RemovePackageCatalogOptions options); } - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] - { - /// Edit an existing Windows Package Catalog. - EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); - } - /// Install the specified package Windows.Foundation.IAsyncOperationWithProgress InstallPackageAsync(CatalogPackage package, InstallOptions options); @@ -1667,6 +1667,12 @@ namespace Microsoft.Management.Deployment // The version of the Windows Package Manager that is running. String Version{ get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + /// Edit an existing Windows Package Catalog. + EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); + } } /// Global settings for PackageManager operations. From cb358bfbf382875d84748b386ea202cbdae02452 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 26 Jan 2026 12:10:12 -0800 Subject: [PATCH 04/15] Add to PS, DSCv2, and GP --- src/AppInstallerSharedLib/GroupPolicy.cpp | 8 +++++++- .../Cmdlets/AddSourceCmdlet.cs | 9 ++++++++- .../Commands/CliCommand.cs | 8 +++++++- .../PSObjects/PSSourceResult.cs | 6 ++++++ .../Microsoft.WinGet.DSC.psm1 | 15 +++++++++++++++ .../tests/Microsoft.WinGet.Client.Tests.ps1 | 3 ++- .../tests/Microsoft.WinGet.DSC.Tests.ps1 | 3 ++- 7 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp index bf2c8fe017..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; } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs index b9b6a4f418..f11de89007 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs @@ -60,13 +60,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 1021c3abf8..6c369ae89a 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(); string parameters = $"add --name \"{name}\" --arg \"{arg}\""; @@ -92,6 +93,11 @@ public void AddSource(string name, string arg, string type, string trustLevel, b parameters += " --explicit"; } + if (priority != 0) + { + parameters += $" --priority \"{priority}\""; + } + _ = this.Run("source", parameters, 300000); } 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..c1d92d0ffd 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -188,7 +188,7 @@ Describe 'Reset-WinGetSource' { Describe 'Get|Add|Reset-WinGetSource' { BeforeAll { - Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -TrustLevel 'Trusted' -Explicit + Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -TrustLevel 'Trusted' -Explicit -Priority 42 } It 'Get Test source' { @@ -200,6 +200,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' { diff --git a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 index 7be3009dd8..833afc06f0 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 @@ -148,7 +148,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 +159,7 @@ Describe 'WinGetSource' { $result.Argument | Should -Be $testSourceArg $result.TrustLevel | Should -Be 'Trusted' $result.Explicit | Should -Be $true + $result.Priority | Should -Be 42 } } From e3b9daa00d483c029aee23888b5206ae00726215 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 26 Jan 2026 17:05:07 -0800 Subject: [PATCH 05/15] Just a comment on next steps --- src/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 10850b3a40..fbf250fb28 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -1312,6 +1312,15 @@ namespace AppInstaller::CLI::Workflow if (searchResult.Matches.size() > 1) { + // Check if we got only one match from the highest priority source + // For each package, get Source Priority + // This will come from DefaultInstallVersion, which should indicate the source of upgrade from an already installed package + // Collect all packages from the highest Source Priority + // If only one, that is the package and move forward + // If multiple + // If all results, continue with current flow (maybe update warning string to reference priority?) + // If reduced results, same flow with new warning string about multiple matches from highest priority source(s) + Logging::Telemetry().LogMultiAppMatch(); if (m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || m_operationType == OperationType::Repair || m_operationType == OperationType::Export) From a85af29481e44b33ed3da36c4bb51e27d822b2ce Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 29 Jan 2026 17:26:26 -0800 Subject: [PATCH 06/15] Initial install resolution work; needs shared method to get priority --- src/AppInstallerCLICore/Resources.h | 1 + .../Workflows/WorkflowBase.cpp | 85 ++++++++++++++++--- .../Shared/Strings/en-us/winget.resw | 4 + .../Commands/Common/PackageCommand.cs | 65 ++++++++++++++ 4 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 1414941032..4afcc159be 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -478,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); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index fbf250fb28..11284579d1 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -470,6 +470,37 @@ namespace AppInstaller::CLI::Workflow OutputInstalledPackagesTable(context, lines); } } + + std::optional GetMatchSourcePriority(const ResultMatch& match) + { + auto installed = match.Package->GetInstalled(); + + if (installed) + { + auto installedVersion = installed->GetLatestVersion(); + + if (installedVersion) + { + auto installedSource = installedVersion->GetSource(); + + if (installedSource.ContainsAvailablePackages()) + { + return installedSource.GetDetails().Priority; + } + } + } + else + { + auto available = match.Package->GetAvailable(); + + if (!available.empty()) + { + return available.front()->GetSource().GetDetails().Priority; + } + } + + return std::nullopt; + } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -1310,20 +1341,54 @@ namespace AppInstaller::CLI::Workflow { auto& searchResult = context.Get(); - if (searchResult.Matches.size() > 1) + 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)) { - // Check if we got only one match from the highest priority source - // For each package, get Source Priority - // This will come from DefaultInstallVersion, which should indicate the source of upgrade from an already installed package - // Collect all packages from the highest Source Priority - // If only one, that is the package and move forward - // If multiple - // If all results, continue with current flow (maybe update warning string to reference priority?) - // If reduced results, same flow with new warning string about multiple matches from highest priority source(s) + // 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 = GetMatchSourcePriority(match); + // 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/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 5dbed873f2..f247bcf6c8 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3546,4 +3546,8 @@ An unlocalized JSON fragment will follow on another line. 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/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs index bc1b7a864b..487312f22e 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs @@ -94,6 +94,32 @@ internal override void SetQueryInFindPackagesOptions( } } + private static int? GetMatchSourcePriority(MatchResult match) + { + var installed = match.CatalogPackage.InstalledVersion; + + if (installed != null) + { + var installedSource = installed.PackageCatalog; + + if (installedSource != null) + { + return installedSource.Info.Priority; + } + } + else + { + auto available = match.CatalogPackage.DefaultInstallVersion + + if (!available.empty()) + { + return available.front()->GetSource().GetDetails().Priority; + } + } + + return null; + } + private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior, PackageFieldMatchOption match) { if (this.CatalogPackage != null) @@ -116,6 +142,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 = GetMatchSourcePriority(result); + + 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); } From 79c4888f5608a4d8c7703cbde09bcf57a6bc2d09 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 30 Jan 2026 16:54:51 -0800 Subject: [PATCH 07/15] Shared priority resolution and beginnings of REST match criteria --- .../Workflows/WorkflowBase.cpp | 33 +--- .../AppInstallerRepositoryCore.vcxproj | 5 +- ...AppInstallerRepositoryCore.vcxproj.filters | 6 + .../MatchCriteriaResolver.cpp | 156 ++++++++++++++++++ .../MatchCriteriaResolver.h | 10 ++ .../PackageVersionSelection.cpp | 29 ++++ .../Public/winget/PackageVersionSelection.h | 3 + .../AppInstallerStrings.cpp | 9 + .../Public/AppInstallerStrings.h | 3 + .../CatalogPackage.cpp | 5 + .../CatalogPackage.h | 98 +++++------ .../PackageManager.idl | 11 +- .../Commands/Common/PackageCommand.cs | 28 +--- 13 files changed, 283 insertions(+), 113 deletions(-) create mode 100644 src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp create mode 100644 src/AppInstallerRepositoryCore/MatchCriteriaResolver.h diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 11284579d1..09b6e2b100 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -470,37 +470,6 @@ namespace AppInstaller::CLI::Workflow OutputInstalledPackagesTable(context, lines); } } - - std::optional GetMatchSourcePriority(const ResultMatch& match) - { - auto installed = match.Package->GetInstalled(); - - if (installed) - { - auto installedVersion = installed->GetLatestVersion(); - - if (installedVersion) - { - auto installedSource = installedVersion->GetSource(); - - if (installedSource.ContainsAvailablePackages()) - { - return installedSource.GetDetails().Priority; - } - } - } - else - { - auto available = match.Package->GetAvailable(); - - if (!available.empty()) - { - return available.front()->GetSource().GetDetails().Priority; - } - } - - return std::nullopt; - } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -1354,7 +1323,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& match : searchResult.Matches) { - std::optional priority = GetMatchSourcePriority(match); + std::optional priority = GetSourcePriority(match.Package); // Optional provides overloads that make empty less than valued and empties equal. if (highestPriority < priority) 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..daca95977d --- /dev/null +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp @@ -0,0 +1,156 @@ +// 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_AlwaysTrue(const Utility::NormalizedString&, const Utility::NormalizedString&) + { + return true; + } + + 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: + // Benefit of the doubt here... + return ValueMatchFunction_AlwaysTrue; + default: + return ValueMatchFunction_AlwaysFalse; + } + } + + // Checks that the match query provided is consistent with the values given. + bool CheckMatchValue(const RequestMatch& query, const Utility::NormalizedString& value, const std::optional& additional) + { + auto matchFunction = GetMatchTypeFunction(query.Type); + + if (matchFunction(value, query.Value)) + { + if (query.Additional) + { + if (additional) + { + return matchFunction(additional.value(), query.Additional.value()); + } + else + { + return false; + } + } + else + { + return true; + } + } + else + { + return false; + } + } + + // Ensures the consistency of the criteria itself. + bool CheckCriteria(const PackageMatchFilter& criteria) + { + // Could also ensure that all enums are valid values + return !criteria.Value.empty(); + } + + // Ensures that the query matches the criteria. + bool CheckQuery(const std::optional& query, const PackageMatchFilter& criteria) + { + if (query && !query->Value.empty()) + { + return CheckMatchValue(query.value(), criteria.Value, criteria.Additional); + } + + return true; + } + + // Ensures that the criteria matches one of the inclusions if provided. + bool CheckInclusions(const std::vector& inclusions, const PackageMatchFilter& criteria) + { + if (inclusions.empty()) + { + return true; + } + + for (const auto& inclusion : inclusions) + { + if (inclusion.Field == criteria.Field && + CheckMatchValue(inclusion, criteria.Value, criteria.Additional)) + { + return true; + } + } + + return false; + } + + // Ensures that the criteria doesn't match one of the filters if provided. + bool CheckFilters(const std::vector& filters, const PackageMatchFilter& criteria) + { + if (filters.empty()) + { + return true; + } + + for (const auto& filter : filters) + { + if (filter.Field == criteria.Field && + CheckMatchValue(filter, criteria.Value, criteria.Additional)) + { + return false; + } + } + + return true; + } + } + + PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const std::shared_ptr& package) + { + + } +} diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h new file mode 100644 index 0000000000..dc91065829 --- /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 std::shared_ptr& package); +} 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/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index c895973e02..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); diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 929db972a4..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); 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/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index e82b4eeb0c..73dab44b81 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -638,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, 28)] + { + /// 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 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 487312f22e..62f8c6ce22 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs @@ -94,32 +94,6 @@ internal override void SetQueryInFindPackagesOptions( } } - private static int? GetMatchSourcePriority(MatchResult match) - { - var installed = match.CatalogPackage.InstalledVersion; - - if (installed != null) - { - var installedSource = installed.PackageCatalog; - - if (installedSource != null) - { - return installedSource.Info.Priority; - } - } - else - { - auto available = match.CatalogPackage.DefaultInstallVersion - - if (!available.empty()) - { - return available.front()->GetSource().GetDetails().Priority; - } - } - - return null; - } - private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior, PackageFieldMatchOption match) { if (this.CatalogPackage != null) @@ -150,7 +124,7 @@ private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior, Packa for (int i = 0; i < results.Count; i++) { MatchResult result = results[i]; - int? priority = GetMatchSourcePriority(result); + int? priority = result.CatalogPackage.CatalogPriority; if ((highestPriority == null && priority != null) || highestPriority < priority) { From fb58e1443cfd50ce78b99c9eccc59df22ecbda88 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 2 Feb 2026 11:05:39 -0800 Subject: [PATCH 08/15] Starting changes for resolver --- .../MatchCriteriaResolver.cpp | 55 ++++++++++++++----- .../MatchCriteriaResolver.h | 4 +- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp index daca95977d..0bdc9b0f4a 100644 --- a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp @@ -14,11 +14,6 @@ namespace AppInstaller::Repository return false; } - bool ValueMatchFunction_AlwaysTrue(const Utility::NormalizedString&, const Utility::NormalizedString&) - { - return true; - } - bool ValueMatchFunction_Exact(const Utility::NormalizedString& a, const Utility::NormalizedString& b) { return a == b; @@ -54,8 +49,6 @@ namespace AppInstaller::Repository case MatchType::Fuzzy: case MatchType::FuzzySubstring: case MatchType::Wildcard: - // Benefit of the doubt here... - return ValueMatchFunction_AlwaysTrue; default: return ValueMatchFunction_AlwaysFalse; } @@ -90,13 +83,6 @@ namespace AppInstaller::Repository } } - // Ensures the consistency of the criteria itself. - bool CheckCriteria(const PackageMatchFilter& criteria) - { - // Could also ensure that all enums are valid values - return !criteria.Value.empty(); - } - // Ensures that the query matches the criteria. bool CheckQuery(const std::optional& query, const PackageMatchFilter& criteria) { @@ -147,10 +133,49 @@ namespace AppInstaller::Repository return true; } + + // ----------------------- NEW + + 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); + } + } + + std::optional GetBestMatchType(const SearchRequest& request, PackageMatchField field, const Utility::NormalizedString& value) + { + + } } - PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const std::shared_ptr& package) + 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; + } + } + + // Multi-value fields + for (auto field : { PackageMatchField::Command, PackageMatchField::Tag, PackageMatchField::PackageFamilyName, + PackageMatchField::ProductCode, PackageMatchField::UpgradeCode }) + { + + } } } diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h index dc91065829..6ad225275f 100644 --- a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.h @@ -5,6 +5,6 @@ namespace AppInstaller::Repository { - // Finds the highest rated match criteria for the package based on the search request - PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const std::shared_ptr& package); + // Finds the highest rated match criteria for the package based on the search request, + PackageMatchFilter FindBestMatchCriteria(const SearchRequest& request, const IPackageVersion* packageVersion); } From a85335cf8a6bd8c63085021fca65b98d5b72df0f Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 2 Feb 2026 17:52:56 -0800 Subject: [PATCH 09/15] REST uses match criteria resolver --- .../MatchCriteriaResolver.cpp | 198 +++++++++++------- .../Rest/RestSource.cpp | 7 +- 2 files changed, 120 insertions(+), 85 deletions(-) diff --git a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp index 0bdc9b0f4a..48074c62e9 100644 --- a/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp +++ b/src/AppInstallerRepositoryCore/MatchCriteriaResolver.cpp @@ -54,106 +54,118 @@ namespace AppInstaller::Repository } } - // Checks that the match query provided is consistent with the values given. - bool CheckMatchValue(const RequestMatch& query, const Utility::NormalizedString& value, const std::optional& additional) + PackageVersionProperty GetPackageVersionPropertyFor(PackageMatchField field) { - auto matchFunction = GetMatchTypeFunction(query.Type); - - if (matchFunction(value, query.Value)) - { - if (query.Additional) - { - if (additional) - { - return matchFunction(additional.value(), query.Additional.value()); - } - else - { - return false; - } - } - else - { - return true; - } - } - else + switch (field) { - return false; + case PackageMatchField::Id: + return PackageVersionProperty::Id; + case PackageMatchField::Name: + return PackageVersionProperty::Name; + case PackageMatchField::Moniker: + return PackageVersionProperty::Moniker; + default: + THROW_HR(E_UNEXPECTED); } } - // Ensures that the query matches the criteria. - bool CheckQuery(const std::optional& query, const PackageMatchFilter& criteria) + PackageVersionMultiProperty GetPackageVersionMultiPropertyFor(PackageMatchField field) { - if (query && !query->Value.empty()) + switch (field) { - return CheckMatchValue(query.value(), criteria.Value, criteria.Additional); + 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); } - - return true; } - - // Ensures that the criteria matches one of the inclusions if provided. - bool CheckInclusions(const std::vector& inclusions, const PackageMatchFilter& criteria) - { - if (inclusions.empty()) + + // 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 true; - } - - for (const auto& inclusion : inclusions) + return std::nullopt; + } + + for (auto matchType : { MatchType::Exact, MatchType::CaseInsensitive, MatchType::StartsWith, MatchType::Substring }) { - if (inclusion.Field == criteria.Field && - CheckMatchValue(inclusion, criteria.Value, criteria.Additional)) + if (matchType >= mustBeBetterThanMatchType) + { + break; + } + + auto matchFunction = GetMatchTypeFunction(matchType); + + if (matchFunction(value, request.Value)) { - return true; + return matchType; } - } + } - return false; + return std::nullopt; } - - // Ensures that the criteria doesn't match one of the filters if provided. - bool CheckFilters(const std::vector& filters, const PackageMatchFilter& criteria) - { - if (filters.empty()) + + // 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) { - return true; - } - - for (const auto& filter : filters) + result = GetBestMatchType(request.Query.value(), mustBeBetterThanMatchType, value); + + if (result) + { + mustBeBetterThanMatchType = result.value(); + } + } + + for (const auto& filter : request.Filters) { - if (filter.Field == criteria.Field && - CheckMatchValue(filter, criteria.Value, criteria.Additional)) + if (result.value_or(MatchType::Wildcard) == MatchType::Exact) { - return false; + break; + } + + if (filter.Field == field) + { + std::optional filterResult = GetBestMatchType(filter, mustBeBetterThanMatchType, value); + + if (filterResult) + { + result = std::move(filterResult); + mustBeBetterThanMatchType = result.value(); + } } - } - - return true; - } - - // ----------------------- NEW - - PackageVersionProperty GetPackageVersionPropertyFor(PackageMatchField field) - { - switch (field) + } + + 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) { - case PackageMatchField::Id: - return PackageVersionProperty::Id; - case PackageMatchField::Name: - return PackageVersionProperty::Name; - case PackageMatchField::Moniker: - return PackageVersionProperty::Moniker; - default: - THROW_HR(E_UNEXPECTED); - } - } - - std::optional GetBestMatchType(const SearchRequest& request, PackageMatchField field, const Utility::NormalizedString& value) - { - + result.Type = bestMatch.value(); + result.Field = field; + result.Value = std::move(normalizedValue); + } + + return MatchType::Exact == result.Type; } } @@ -168,6 +180,11 @@ namespace AppInstaller::Repository if (propertyValue.empty()) { continue; + } + + if (UpdatePackageMatchFilterCheck(request, field, result, propertyValue)) + { + break; } } @@ -175,7 +192,26 @@ namespace AppInstaller::Repository 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/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)); } From eba2c05bca1a730e69d034a434b53cb1585012f5 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 3 Feb 2026 10:42:00 -0800 Subject: [PATCH 10/15] Release notes, remove unmerged experimental use, move COM to contract 29 --- doc/ReleaseNotes.md | 68 ++++++++----------- .../Commands/DscSourceResource.cpp | 3 +- .../PackageManager.idl | 15 ++-- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index d899cd2d67..31c2bc8e3d 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,43 +1,29 @@ -## New in v1.28 - -* Bumped the winget version to 1.28 to match the package version. -* Additional [options for limiting the size of log files](https://github.com/microsoft/winget-cli/blob/master/doc/Settings.md#file). - -# New Feature: 'source edit' -New feature that adds an 'edit' subcommand to the 'source' command. This can be used to set an explicit source to be implicit and vice-versa. For example, with this feature you can make the 'winget-font' source an implicit source instead of explicit source. - -To use the feature, try `winget source edit winget-font` to set the Explicit state to the default. - -# New Experimental Feature: 'listDetails' - -The new experimental feature `listDetails` enables a new option for the `list` command, `--details`. When supplied, the output is no longer a table view of the results but is instead a series of `show` like outputs drawing data from the installed item. - -An example output for a single installed package is: -```PowerShell -> wingetdev list Microsoft.VisualStudio.2022.Enterprise --details -Visual Studio Enterprise 2022 [Microsoft.VisualStudio.2022.Enterprise] -Version: 17.14.21 (November 2025) -Publisher: Microsoft Corporation -Local Identifier: ARP\Machine\X86\875fed29 -Product Code: 875fed29 -Installer Category: exe -Installed Scope: Machine -Installed Location: C:\Program Files\Microsoft Visual Studio\2022\Enterprise -Available Upgrades: - winget [17.14.23] -``` - -If sixels are enabled and supported by the terminal, an icon for the installed package will be shown. - -To enable this feature, add the 'listDetails' experimental feature to your settings. -``` -"experimentalFeatures": { - "listDetails": true -}, -``` +## 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 minorly 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 -* Portable Packages now use the correct directory separators regardless of which convention is used in the manifest -* `--suppress-initial-details` now works with `winget configure test` -* `--suppress-initial-details` no longer requires `--accept-configuration-agreements` -* Corrected property of `Font` experimental feature to accurately reflect `fonts` as the required setting value diff --git a/src/AppInstallerCLICore/Commands/DscSourceResource.cpp b/src/AppInstallerCLICore/Commands/DscSourceResource.cpp index 87a24e475c..461c2c0efd 100644 --- a/src/AppInstallerCLICore/Commands/DscSourceResource.cpp +++ b/src/AppInstallerCLICore/Commands/DscSourceResource.cpp @@ -217,8 +217,7 @@ namespace AppInstaller::CLI AICLI_LOG(CLI, Verbose, << "Source::Replace invoked"); // Check to see if we can use an edit rather than a complete replacement - if (TestArgument() && TestType() && TestTrustLevel() && - Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SourceEdit)) + if (TestArgument() && TestType() && TestTrustLevel()) { // Implies that the failing portion of Test was in the editable Explicit or Priority properties Edit(); diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 73dab44b81..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 @@ -336,7 +336,7 @@ namespace Microsoft.Management.Deployment Boolean Explicit{ get; }; } - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] { /// The priority of this catalog. Higher values are sorted first. Int32 Priority{ get; }; @@ -638,7 +638,7 @@ namespace Microsoft.Management.Deployment CheckInstalledStatusResult CheckInstalledStatus(); } - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + [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. @@ -1483,7 +1483,7 @@ namespace Microsoft.Management.Deployment /// Excludes a source from discovery unless specified. Boolean Explicit; - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] { /// The priority of this catalog. Higher values are sorted first. Int32 Priority; @@ -1570,8 +1570,11 @@ namespace Microsoft.Management.Deployment /// Editing the Explicit property has three states: true, false, and not specified (null). Windows.Foundation.IReference Explicit; - /// The priority of this catalog. Higher values are sorted first. - Windows.Foundation.IReference Priority; + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// The priority of this catalog. Higher values are sorted first. + Windows.Foundation.IReference Priority; + } }; /// IMPLEMENTATION NOTE: RemovePackageCatalogStatus From 92d688a28b8d60724c4b9cc30e73bbf93582bc73 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 3 Feb 2026 16:39:35 -0800 Subject: [PATCH 11/15] Match tests --- doc/ReleaseNotes.md | 2 +- .../DSCv3SourceResourceCommand.cs | 1 - .../Helpers/WinGetSettingsHelper.cs | 1 - src/AppInstallerCLIE2ETests/SourceCommand.cs | 1 - .../AppInstallerCLITests.vcxproj | 1 + .../AppInstallerCLITests.vcxproj.filters | 3 + .../MatchCriteriaResolver.cpp | 154 ++++++++++++++++++ src/AppInstallerCLITests/TestSource.cpp | 27 +++ 8 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 src/AppInstallerCLITests/MatchCriteriaResolver.cpp diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 31c2bc8e3d..a793a3e3d4 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -15,7 +15,7 @@ in the results if other things are equal. > 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 minorly affect the result ordering, commands that primarily target available packages +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 diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs index 09e4e6f884..8eeca0da23 100644 --- a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -78,7 +78,6 @@ public void OneTimeTeardown() public void Setup() { RemoveTestSource(); - WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index b7e9638835..aa3420782e 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -227,7 +227,6 @@ public static void InitializeAllFeatures(bool status) ConfigureFeature(settingsJson, "resume", status); ConfigureFeature(settingsJson, "reboot", status); ConfigureFeature(settingsJson, "fonts", status); - ConfigureFeature(settingsJson, "sourceEdit", status); ConfigureFeature(settingsJson, "sourcePriority", status); SetWingetSettings(settingsJson); diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index 46f4eb4ab1..ee4207e366 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -20,7 +20,6 @@ public class SourceCommand : BaseCommand [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); WinGetSettingsHelper.ConfigureFeature("sourcePriority", true); } 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/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index ef5e4ce828..f8ad1c7cfa 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; From 693d574a50763fa7e59ba87826701298825d9f1d Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 3 Feb 2026 17:31:27 -0800 Subject: [PATCH 12/15] Test infra source --- .../ConfigurableTestSourceFactory.cpp | 163 +++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) 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; }; From 6b9504e6ed495b99d156e32681fccb46a5a3960d Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 4 Feb 2026 09:22:03 -0800 Subject: [PATCH 13/15] Add e2e tests for priority install --- src/AppInstallerCLIE2ETests/InstallCommand.cs | 41 +++++++++++++++++++ .../tests/Microsoft.WinGet.Client.Tests.ps1 | 40 ++++++++++++++++++ 2 files changed, 81 insertions(+) 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/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index c1d92d0ffd..d8b559992a 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -494,6 +494,46 @@ 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' { + 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 { + Remove-WinGetSource -Name 'dummyPackageSource' + RemoveTestSource + + $testExe = Get-WinGetPackage -Id AppInstallerTest.TestExeInstaller -MatchOption Equals + if ($testExe.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.TestExeInstaller + } + } +} + Describe 'Get-WinGetPackage' { BeforeAll { From 603b97b722fb5f259497d46011d0042e5a2ef519 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 5 Feb 2026 11:29:50 -0800 Subject: [PATCH 14/15] PR feedback --- src/AppInstallerCLI.sln | 4 +-- .../DSCv3SourceResourceCommand.cs | 7 ++++- .../Shared/Strings/en-us/winget.resw | 2 +- src/AppInstallerCLITests/Sources.cpp | 31 ++++++++++--------- src/AppInstallerCLITests/TestSource.cpp | 5 ++- .../Cmdlets/AddSourceCmdlet.cs | 7 +++++ .../tests/Microsoft.WinGet.Client.Tests.ps1 | 10 ++++-- .../tests/Microsoft.WinGet.DSC.Tests.ps1 | 2 ++ 8 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 7638d486e6..ab0f2f6c23 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36811.4 d17.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "AppInstallerCLIPackage", "AppInstallerCLIPackage\AppInstallerCLIPackage.wapproj", "{6AA3791A-0713-4548-A357-87A323E7AC3A}" ProjectSection(ProjectDependencies) = postProject diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs index 8eeca0da23..dc5d9e7061 100644 --- a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -489,7 +489,12 @@ private static void AssertExistingSourceResourceData(SourceResourceData output, 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) diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index f247bcf6c8..46eee78281 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3536,7 +3536,7 @@ An unlocalized JSON fragment will follow on another line. Column title for listing the new value. - Priority with higher numbers first + Priority; higher numbers first This argument sets the numerical priority of the source. Higher values will be first in priority. diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index 8f524121fa..a92683ce71 100644 --- a/src/AppInstallerCLITests/Sources.cpp +++ b/src/AppInstallerCLITests/Sources.cpp @@ -388,12 +388,10 @@ TEST_CASE("RepoSources_ThreeSources", "[sources]") SetSetting(Stream::UserSources, s_ThreeSources); SetSetting(Stream::SourcesMetadata, s_ThreeSourcesMetadata); - std::vector sources = GetSources(); - REQUIRE(sources.size() == 3); - - const char* suffixUnsorted[3] = { "", "2", "3" }; - const char* suffixPrioritySorted[3] = { "2", "3", "" }; - const char** suffix = nullptr; + 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") @@ -406,15 +404,20 @@ TEST_CASE("RepoSources_ThreeSources", "[sources]") suffix = suffixPrioritySorted; } - for (size_t i = 0; i < 3; ++i) + std::vector sources = GetSources(); + REQUIRE(sources.size() == 3); + + 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); } } diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index f8ad1c7cfa..e900dc556a 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -489,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, {} }; + 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/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs index f11de89007..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; } /// diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index d8b559992a..44f4cc9c47 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -504,6 +504,9 @@ Describe 'Install-WinGetPackage Source Priority' { } 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}' @@ -523,14 +526,15 @@ Describe 'Install-WinGetPackage Source Priority' { } AfterEach { - Remove-WinGetSource -Name 'dummyPackageSource' - RemoveTestSource - $testExe = Get-WinGetPackage -Id AppInstallerTest.TestExeInstaller -MatchOption Equals if ($testExe.Count -gt 0) { Uninstall-WinGetPackage -Id AppInstallerTest.TestExeInstaller } + + Remove-WinGetSource -Name 'dummyPackageSource' + RemoveTestSource + RestoreWinGetSettings } } diff --git a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 index 833afc06f0..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' From c5c0006d2a6b75481059ada0f5fb8d971fa6d0b4 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 5 Feb 2026 14:31:41 -0800 Subject: [PATCH 15/15] Test fix --- src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index 44f4cc9c47..f8042c2565 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -188,6 +188,9 @@ Describe 'Reset-WinGetSource' { Describe 'Get|Add|Reset-WinGetSource' { BeforeAll { + $ogSettings = @{ experimentalFeatures= @{sourcePriority=$true}} + SetWinGetSettingsHelper $ogSettings + Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' -TrustLevel 'Trusted' -Explicit -Priority 42 } @@ -211,6 +214,11 @@ Describe 'Get|Add|Reset-WinGetSource' { It 'Reset Test source' { Reset-WinGetSource -Name TestSource } + + AfterAll { + RemoveTestSource + RestoreWinGetSettings + } } Describe 'Find-WinGetPackage' {