diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 142236ace8..97dfd9c371 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -47,6 +47,16 @@ The WinGet MCP server's existing tools have been extended with new parameters to The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` environment variables to authenticate GitHub API requests. This significantly increases the GitHub API rate limit, preventing failures in CI/CD pipelines. Use `-Verbose` to see which token is being used. +### Default priority of installer types + +Installer type selection no longer depends on the order defined on the manifest. Instead, preference is given in this order: +- MSIX +- MSI / Wix / Burn +- Nullsoft / Inno / EXE +- Portable + +When a user configures installer type requirements or preferences, the order in which they are listed is now respected during installer selection. + ### Improved `list` output when redirected - `winget list` (and similar table commands) no longer truncates output when stdout is redirected to a file or variable — column widths are now computed from the full result set. diff --git a/doc/Settings.md b/doc/Settings.md index 4915f81afa..189761ec39 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -137,6 +137,8 @@ The `archiveExtractionMethod` behavior affects how installer archives are extrac Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. +When multiple values are listed under `requirements`, they are treated as an ordered preference in addition to a filter — the first listed value is preferred over subsequent ones when multiple valid options exist. If both `requirements` and `preferences` are set for the same field, the ordering from `preferences` takes precedence. + Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. > [!NOTE] @@ -186,12 +188,15 @@ The `installerTypes` behavior affects what installer types will be selected when Allowed values as of version 1.12.470 include: `appx`, `burn`, `exe`, `font`, `inno`, `msi`, `msix`, `msstore`, `nullsoft`, `portable`, `wix`, `zip` -By default, and with all other properties being equal, WinGet defaults to the installer type that is listed first in the manifest's installer YAML if the package has not been installed yet. If it is already installed, the same installer type will be required to ensure a proper upgrade. +By default, when no user preference or requirement is configured, WinGet selects installer types in the following order: MSIX, MSI/Wix/Burn, Nullsoft/Inno/EXE, Portable. If it is already installed, the same installer type will be required to ensure a proper upgrade. ```json "installBehavior": { "preferences": { "installerTypes": ["msi", "msix"] + }, + "requirements": { + "installerTypes": ["msix", "msi"] } }, ``` diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp index 9a7f08db01..5555c52eec 100644 --- a/src/AppInstallerCLITests/ManifestComparator.cpp +++ b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -199,8 +199,8 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); - // Only because it is first - RequireInstaller(result, msi); + // Msix is preferred over Msi by the default installer type precedence order + RequireInstaller(result, msix); REQUIRE(inapplicabilities.size() == 0); } SECTION("MSI Installed") @@ -238,7 +238,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); - // Only because it is first + // Burn is preferred over Exe by the default installer type precedence order RequireInstaller(result, burn); REQUIRE(inapplicabilities.size() == 0); } @@ -844,6 +844,44 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") RequireInstaller(result, exe); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); } + SECTION("Multiple requirements - first requirement wins") + { + // Requirements act as both a filter and an ordering guide; Exe is listed first so it wins. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } + SECTION("Multiple requirements alternate order") + { + // Same requirements as above but Msix is listed first — Msix should win. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } + SECTION("Requirements and preferences coexist - preference ordering wins") + { + // When both are set, preference ordering takes precedence over requirement ordering. + // Preferences say Msix first; requirements say Exe first — Msix should win. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); + settings.Set({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } SECTION("Inno requirement") { TestUserSettings settings; @@ -855,6 +893,31 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") REQUIRE(!result); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); } + SECTION("No user preference - default order applies") + { + // No TestUserSettings means no user-configured preferences; the default order should be used. + // Default order: MSStore > Msix > Msi > Wix > Burn > Nullsoft > Inno > Exe > Portable + // Manifest has Msi, Exe, Msix — Msix should win. + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("No user preference - type not in default list does not win over default-ordered types") + { + // Zip is not in the default preference list; it should not be preferred over Msix/Msi/Exe. + Manifest localManifest; + ManifestInstaller zip = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Zip, ScopeEnum::User); + ManifestInstaller localExe = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::User); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(localManifest); + + // Exe is in the default list; Zip is not — Exe should be preferred. + RequireInstaller(result, localExe); + REQUIRE(inapplicabilities.size() == 0); + } } TEST_CASE("ManifestComparator_MachineArchitecture_Strong_Scope_Weak", "[manifest_comparator]") diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp index 92bd27f0aa..819b72cffb 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -228,6 +228,22 @@ namespace AppInstaller::Manifest { preference = Settings::User().Get(); requirement = Settings::User().Get(); + + // Apply default precedence order when the user has not configured any installer type preferences or requirements. + if (preference.empty() && requirement.empty()) + { + preference = { + InstallerTypeEnum::MSStore, + InstallerTypeEnum::Msix, + InstallerTypeEnum::Msi, + InstallerTypeEnum::Wix, + InstallerTypeEnum::Burn, + InstallerTypeEnum::Nullsoft, + InstallerTypeEnum::Inno, + InstallerTypeEnum::Exe, + InstallerTypeEnum::Portable, + }; + } } if (!preference.empty() || !requirement.empty()) @@ -270,12 +286,15 @@ namespace AppInstaller::Manifest details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override { - if (m_preference.empty()) + // If no preferences are set, use requirement ordering instead. + const auto& effectiveOrder = m_preference.empty() ? m_requirement : m_preference; + + if (effectiveOrder.empty()) { return details::ComparisonResult::Negative; } - for (InstallerTypeEnum installerTypePreference : m_preference) + for (InstallerTypeEnum installerTypePreference : effectiveOrder) { bool isFirstInstallerTypePreferred = first.EffectiveInstallerType() == installerTypePreference ||