Skip to content
10 changes: 10 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"]
}
},
```
Expand Down
69 changes: 66 additions & 3 deletions src/AppInstallerCLITests/ManifestComparator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<Setting::InstallerTypeRequirement>({ 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<Setting::InstallerTypeRequirement>({ 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<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix });
settings.Set<Setting::InstallerTypePreference>({ 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;
Expand All @@ -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]")
Expand Down
23 changes: 21 additions & 2 deletions src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,22 @@ namespace AppInstaller::Manifest
{
preference = Settings::User().Get<Settings::Setting::InstallerTypePreference>();
requirement = Settings::User().Get<Settings::Setting::InstallerTypeRequirement>();

// Apply default precedence order when the user has not configured any installer type preferences or requirements.
if (preference.empty() && requirement.empty())
Comment thread
Trenly marked this conversation as resolved.
{
preference = {
InstallerTypeEnum::MSStore,
InstallerTypeEnum::Msix,
InstallerTypeEnum::Msi,
InstallerTypeEnum::Wix,
InstallerTypeEnum::Burn,
InstallerTypeEnum::Nullsoft,
InstallerTypeEnum::Inno,
InstallerTypeEnum::Exe,
InstallerTypeEnum::Portable,
};
}
}

if (!preference.empty() || !requirement.empty())
Expand Down Expand Up @@ -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 ||
Expand Down