Skip to content

Commit 2e56941

Browse files
authored
Add default installer precedence if not defined by user (#6123)
This PR adds a default order of precedence for installer types if the user has not specified one. This ensures that ordering in a manifest is less important. While this has the potential to cause some upset with publishers who would prefer a different default order, the linked issue can remain open for tracking those requests. Future work could involve merging the default preferences with user preferences so that any installer types the user has not specified as their preference still adhere to this default ordering, but I presumed that to be out of scope for this feature
1 parent b94fcbc commit 2e56941

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

doc/ReleaseNotes.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ The WinGet MCP server's existing tools have been extended with new parameters to
4747

4848
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.
4949

50+
### Default priority of installer types
51+
52+
Installer type selection no longer depends on the order defined on the manifest. Instead, preference is given in this order:
53+
- MSIX
54+
- MSI / Wix / Burn
55+
- Nullsoft / Inno / EXE
56+
- Portable
57+
58+
When a user configures installer type requirements or preferences, the order in which they are listed is now respected during installer selection.
59+
5060
### Improved `list` output when redirected
5161

5262
- `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.

doc/Settings.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ The `archiveExtractionMethod` behavior affects how installer archives are extrac
137137

138138
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.
139139

140+
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.
141+
140142
Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command.
141143

142144
> [!NOTE]
@@ -186,12 +188,15 @@ The `installerTypes` behavior affects what installer types will be selected when
186188

187189
Allowed values as of version 1.12.470 include: `appx`, `burn`, `exe`, `font`, `inno`, `msi`, `msix`, `msstore`, `nullsoft`, `portable`, `wix`, `zip`
188190

189-
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.
191+
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.
190192

191193
```json
192194
"installBehavior": {
193195
"preferences": {
194196
"installerTypes": ["msi", "msix"]
197+
},
198+
"requirements": {
199+
"installerTypes": ["msix", "msi"]
195200
}
196201
},
197202
```

src/AppInstallerCLITests/ManifestComparator.cpp

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]")
199199
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
200200
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
201201

202-
// Only because it is first
203-
RequireInstaller(result, msi);
202+
// Msix is preferred over Msi by the default installer type precedence order
203+
RequireInstaller(result, msix);
204204
REQUIRE(inapplicabilities.size() == 0);
205205
}
206206
SECTION("MSI Installed")
@@ -238,7 +238,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]")
238238
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
239239
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
240240

241-
// Only because it is first
241+
// Burn is preferred over Exe by the default installer type precedence order
242242
RequireInstaller(result, burn);
243243
REQUIRE(inapplicabilities.size() == 0);
244244
}
@@ -844,6 +844,44 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]")
844844
RequireInstaller(result, exe);
845845
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType });
846846
}
847+
SECTION("Multiple requirements - first requirement wins")
848+
{
849+
// Requirements act as both a filter and an ordering guide; Exe is listed first so it wins.
850+
TestUserSettings settings;
851+
settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix });
852+
853+
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
854+
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
855+
856+
RequireInstaller(result, exe);
857+
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType });
858+
}
859+
SECTION("Multiple requirements alternate order")
860+
{
861+
// Same requirements as above but Msix is listed first — Msix should win.
862+
TestUserSettings settings;
863+
settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe });
864+
865+
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
866+
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
867+
868+
RequireInstaller(result, msix);
869+
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType });
870+
}
871+
SECTION("Requirements and preferences coexist - preference ordering wins")
872+
{
873+
// When both are set, preference ordering takes precedence over requirement ordering.
874+
// Preferences say Msix first; requirements say Exe first — Msix should win.
875+
TestUserSettings settings;
876+
settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix });
877+
settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe });
878+
879+
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
880+
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
881+
882+
RequireInstaller(result, msix);
883+
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType });
884+
}
847885
SECTION("Inno requirement")
848886
{
849887
TestUserSettings settings;
@@ -855,6 +893,31 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]")
855893
REQUIRE(!result);
856894
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType });
857895
}
896+
SECTION("No user preference - default order applies")
897+
{
898+
// No TestUserSettings means no user-configured preferences; the default order should be used.
899+
// Default order: MSStore > Msix > Msi > Wix > Burn > Nullsoft > Inno > Exe > Portable
900+
// Manifest has Msi, Exe, Msix — Msix should win.
901+
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
902+
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);
903+
904+
RequireInstaller(result, msix);
905+
REQUIRE(inapplicabilities.size() == 0);
906+
}
907+
SECTION("No user preference - type not in default list does not win over default-ordered types")
908+
{
909+
// Zip is not in the default preference list; it should not be preferred over Msix/Msi/Exe.
910+
Manifest localManifest;
911+
ManifestInstaller zip = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Zip, ScopeEnum::User);
912+
ManifestInstaller localExe = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::User);
913+
914+
ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
915+
auto [result, inapplicabilities] = mc.GetPreferredInstaller(localManifest);
916+
917+
// Exe is in the default list; Zip is not — Exe should be preferred.
918+
RequireInstaller(result, localExe);
919+
REQUIRE(inapplicabilities.size() == 0);
920+
}
858921
}
859922

860923
TEST_CASE("ManifestComparator_MachineArchitecture_Strong_Scope_Weak", "[manifest_comparator]")

src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,22 @@ namespace AppInstaller::Manifest
228228
{
229229
preference = Settings::User().Get<Settings::Setting::InstallerTypePreference>();
230230
requirement = Settings::User().Get<Settings::Setting::InstallerTypeRequirement>();
231+
232+
// Apply default precedence order when the user has not configured any installer type preferences or requirements.
233+
if (preference.empty() && requirement.empty())
234+
{
235+
preference = {
236+
InstallerTypeEnum::MSStore,
237+
InstallerTypeEnum::Msix,
238+
InstallerTypeEnum::Msi,
239+
InstallerTypeEnum::Wix,
240+
InstallerTypeEnum::Burn,
241+
InstallerTypeEnum::Nullsoft,
242+
InstallerTypeEnum::Inno,
243+
InstallerTypeEnum::Exe,
244+
InstallerTypeEnum::Portable,
245+
};
246+
}
231247
}
232248

233249
if (!preference.empty() || !requirement.empty())
@@ -270,12 +286,15 @@ namespace AppInstaller::Manifest
270286

271287
details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override
272288
{
273-
if (m_preference.empty())
289+
// If no preferences are set, use requirement ordering instead.
290+
const auto& effectiveOrder = m_preference.empty() ? m_requirement : m_preference;
291+
292+
if (effectiveOrder.empty())
274293
{
275294
return details::ComparisonResult::Negative;
276295
}
277296

278-
for (InstallerTypeEnum installerTypePreference : m_preference)
297+
for (InstallerTypeEnum installerTypePreference : effectiveOrder)
279298
{
280299
bool isFirstInstallerTypePreferred =
281300
first.EffectiveInstallerType() == installerTypePreference ||

0 commit comments

Comments
 (0)