Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ For aliases requested through `--rename`, `Commands`, or `PortableCommandAlias`,
This change resolves alias failures in non-symlinked scenarios, including cases where WinGet adds the install directory to `PATH` instead of creating links.
Because the alias is now created as an executable hardlink in the install location, command aliases remain available and consistent even when symlink creation is skipped.

### OS version specific installers

Manifests can now include installers that differ only by `MinimumOSVersion`, for example to declare different dependencies on different OS versions.
These are no longer rejected as duplicate installer entries, and when more than one is applicable, the installer with the highest `MinimumOSVersion` is preferred regardless of its order in the manifest.
Architecture, scope, locale and installer type preferences still take priority over `MinimumOSVersion`.

For example, a package that needs .NET Framework 3.5 can depend on the `NetFx3` Windows feature on older OS versions, and on a standalone .NET Framework 3.5 package from `10.0.28000.0`:

```yaml
Installers:
- Architecture: x64
InstallerUrl: https://example.com/setup.exe
InstallerSha256: <hash>
Dependencies:
WindowsFeatures:
- NetFx3
- Architecture: x64
InstallerUrl: https://example.com/setup.exe
InstallerSha256: <hash>
MinimumOSVersion: 10.0.28000.0
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.Framework.Runtime.3
```

### Minor Bug Fixes
* Fixed an issue where `winget search --id <msstoreId>` could fail to return a Microsoft Store package unless `--exact` was also provided.
* Updated NUnit to v4
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@
<CopyFileToFolders Include="TestData\Manifest-Bad-InstallerUniqueness-SameLang.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Bad-InstallerUniqueness-SameMinOSVersion.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Bad-InstallerUniqueness.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
Expand Down Expand Up @@ -683,6 +686,9 @@
<CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffLangs.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffMinOSVersion.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffScope.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,9 @@
<CopyFileToFolders Include="TestData\Manifest-Bad-InstallerUniqueness-SameLang.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Bad-InstallerUniqueness-SameMinOSVersion.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Bad-InvalidLocale.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
Expand Down Expand Up @@ -681,6 +684,9 @@
<CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffScope.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Good-InstallerUniqueness-DiffMinOSVersion.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\Manifest-Good-Minimum.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
Expand Down
81 changes: 81 additions & 0 deletions src/AppInstallerCLITests/ManifestComparator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <ExecutionContext.h>
#include <COMContext.h>
#include <winget/ManifestComparator.h>
#include <winget/ManifestYamlParser.h>
#include <winget/Runtime.h>
#include <winget/UserSettings.h>
#include <Workflows/WorkflowBase.h>

Expand Down Expand Up @@ -950,3 +952,82 @@ TEST_CASE("ManifestComparator_InstallerCompatibilitySet_Weaker_Than_Architecture

RequireInstaller(result, target);
}

TEST_CASE("ManifestComparator_OSVersionCompare", "[manifest_comparator]")
{
Manifest manifest;
ManifestInstaller noMinimum;
ManifestInstaller older;
ManifestInstaller newer;

// Installer order should not matter; the installer with the highest applicable MinOSVersion is preferred.
SECTION("Ascending")
{
noMinimum = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "");
older = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.0.0");
newer = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.10240.0");
}
SECTION("Descending")
{
newer = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.10240.0");
older = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.0.0");
noMinimum = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "");
}

ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);

RequireInstaller(result, newer);
REQUIRE(inapplicabilities.size() == 0);
}

TEST_CASE("ManifestComparator_OSVersionCompare_InapplicableIgnored", "[manifest_comparator]")
{
Manifest manifest;
ManifestInstaller noMinimum = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "");
AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.99999.0");

ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);

RequireInstaller(result, noMinimum);
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::OSVersion });
}

TEST_CASE("ManifestComparator_OSVersion_Weaker_Than_Architecture", "[manifest_comparator]")
{
Manifest manifest;
ManifestInstaller target = AddInstaller(manifest, GetSystemArchitecture(), InstallerTypeEnum::Exe, ScopeEnum::Unknown, "");
ManifestInstaller foil = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.10240.0");

ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);

RequireInstaller(result, target);
}

TEST_CASE("ManifestComparator_OSVersion_NetFx3", "[manifest_comparator]")
{
// The NetFx3 Windows feature is not available from 10.0.28000, where .NET Framework 3.5 is a standalone package instead.
Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-InstallerUniqueness-DiffMinOSVersion.yaml"));

ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {}));
auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest);

REQUIRE(result);

if (AppInstaller::Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version{ "10.0.28000.0" }))
{
REQUIRE(result->MinOSVersion == "10.0.28000.0");
REQUIRE(result->Dependencies.HasExactDependency(DependencyType::Package, "Microsoft.DotNet.Framework.Runtime.3"));
REQUIRE(!result->Dependencies.HasAnyOf(DependencyType::WindowsFeature));
REQUIRE(inapplicabilities.size() == 0);
}
else
{
REQUIRE(result->MinOSVersion.empty());
REQUIRE(result->Dependencies.HasExactDependency(DependencyType::WindowsFeature, "NetFx3"));
REQUIRE(!result->Dependencies.HasAnyOf(DependencyType::Package));
RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::OSVersion });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Bad Manifest. Uniqueness installers same type, arch, locale, scope and MinimumOSVersion.
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json

PackageIdentifier: AppInstallerCliTest.TestExeInstaller
PackageVersion: 1.0.0.0
PackageLocale: en-US
PackageName: AppInstaller Test Exe Installer
ShortDescription: AppInstaller Test Exe Installer
Publisher: Microsoft Corporation
License: Test
InstallerType: exe
InstallerSwitches:
Silent: /silent
SilentWithProgress: /silentwithprogress
Installers:
- Architecture: x64
InstallerUrl: https://ThisIsNotUsed
InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD
MinimumOSVersion: 10.0.28000.0
Dependencies:
WindowsFeatures:
- NetFx3
- Architecture: x64
InstallerUrl: https://ThisIsNotUsed
InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD
MinimumOSVersion: 10.0.28000
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.Framework.Runtime.3
ManifestType: singleton
ManifestVersion: 1.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Good Manifest. Uniqueness installers same type, arch, locale and scope but different MinimumOSVersion.
# Models a package that needs the NetFx3 Windows feature on older OS versions, and the standalone
# .NET Framework 3.5 package on OS versions where the feature is no longer available.
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json

PackageIdentifier: AppInstallerCliTest.TestExeInstaller
PackageVersion: 1.0.0.0
PackageLocale: en-US
PackageName: AppInstaller Test Exe Installer
ShortDescription: AppInstaller Test Exe Installer
Publisher: Microsoft Corporation
License: Test
InstallerType: exe
InstallerSwitches:
Silent: /silent
SilentWithProgress: /silentwithprogress
Installers:
- Architecture: x64
InstallerUrl: https://ThisIsNotUsed
InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD
Dependencies:
WindowsFeatures:
- NetFx3
- Architecture: x64
InstallerUrl: https://ThisIsNotUsed
InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD
MinimumOSVersion: 10.0.28000.0
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.Framework.Runtime.3
ManifestType: singleton
ManifestVersion: 1.0.0
2 changes: 2 additions & 0 deletions src/AppInstallerCLITests/YamlManifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ TEST_CASE("ReadGoodManifests", "[ManifestValidation]")
{ "Manifest-Good-InstallerUniqueness-DefaultLang.yaml" },
{ "Manifest-Good-InstallerUniqueness-DiffLangs.yaml" },
{ "Manifest-Good-InstallerUniqueness-DiffScope.yaml" },
{ "Manifest-Good-InstallerUniqueness-DiffMinOSVersion.yaml" },
{ "Manifest-Good-Minimum.yaml" },
{ "Manifest-Good-Minimum-InstallerType.yaml" },
{ "Manifest-Good-Switches.yaml" },
Expand Down Expand Up @@ -921,6 +922,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]")
{ "Manifest-Bad-InstallerUniqueness-DefaultScope.yaml", "Duplicate installer entry found." },
{ "Manifest-Bad-InstallerUniqueness-DefaultValues.yaml", "Duplicate installer entry found." },
{ "Manifest-Bad-InstallerUniqueness-SameLang.yaml", "Duplicate installer entry found." },
{ "Manifest-Bad-InstallerUniqueness-SameMinOSVersion.yaml", "Duplicate installer entry found." },
{ "Manifest-Bad-LicenseMissing.yaml", "Missing required property 'License'" },
{ "Manifest-Bad-NameMissing.yaml", "Missing required property 'Name'" },
{ "Manifest-Bad-PublisherMissing.yaml", "Missing required property 'Publisher'" },
Expand Down
37 changes: 33 additions & 4 deletions src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ namespace AppInstaller::Manifest
}
};

struct OSVersionFilter : public details::FilterField
struct OSVersionComparator : public details::ComparisonField
{
OSVersionFilter() : details::FilterField("OS Version") {}
OSVersionComparator() : details::ComparisonField("OS Version") {}

InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override
{
Expand All @@ -68,6 +68,34 @@ namespace AppInstaller::Manifest
result += installer.MinOSVersion;
return result;
}

details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override
{
// Both installers are applicable to the current OS at this point. The one with the higher MinOSVersion
// targets the current OS more specifically, so it should be preferred over one meant for older OS versions.
if (IsMinOSVersionHigher(first, second))
{
return details::ComparisonResult::WeakPositive;
}

return details::ComparisonResult::Negative;
}

private:
static bool IsMinOSVersionHigher(const ManifestInstaller& first, const ManifestInstaller& second)
{
if (first.MinOSVersion.empty())
{
return false;
}

if (second.MinOSVersion.empty())
{
return true;
}

return Utility::Version(first.MinOSVersion) > Utility::Version(second.MinOSVersion);
}
};

struct MachineArchitectureComparator : public details::ComparisonField
Expand Down Expand Up @@ -735,8 +763,6 @@ namespace AppInstaller::Manifest

ManifestComparator::ManifestComparator(const Options& options)
{
// Filters based on installer's MinOSVersion
AddFilter(std::make_unique<OSVersionFilter>());
// Filters out portable installers if they are not supported by the system
AddFilter(std::make_unique<PortableInstallFilter>());
// Filters based on the scope of a currently installed package
Expand Down Expand Up @@ -777,6 +803,9 @@ namespace AppInstaller::Manifest
// Only applies when preference exists:
// Weak if first is in preference list and second is not
AddComparator(InstallerTypeComparator::Create(options));
// Filters out installers whose MinOSVersion is higher than the current OS version.
// Weak if first has a higher MinOSVersion than second (it targets the current OS more specifically)
AddComparator(std::make_unique<OSVersionComparator>());
}

InstallerAndInapplicabilities ManifestComparator::GetPreferredInstaller(const Manifest& manifest)
Expand Down
21 changes: 19 additions & 2 deletions src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ namespace AppInstaller::Manifest
auto defaultLocErrors = ValidateManifestLocalization(manifest.DefaultLocalization, !options.FullValidation);
std::move(defaultLocErrors.begin(), defaultLocErrors.end(), std::inserter(resultErrors, resultErrors.end()));

// Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key.
// Comparison function to check duplicate installer entry. {installerType, arch, language, scope and minOSVersion} combination is the key.
// MinOSVersion is part of the key so that a manifest can provide different installers (or dependencies) for different OS versions.
// Todo: use the comparator from ManifestComparator when that one is fully implemented.
auto installerCmp = [](const ManifestInstaller& in1, const ManifestInstaller& in2)
{
Expand Down Expand Up @@ -277,6 +278,22 @@ namespace AppInstaller::Manifest
return in1.Scope < in2.Scope;
}

// An empty MinOSVersion is treated as the lowest possible version.
if (in1.MinOSVersion.empty() != in2.MinOSVersion.empty())
{
return in1.MinOSVersion.empty();
}

if (!in1.MinOSVersion.empty())
{
Utility::Version minOSVersion1{ in1.MinOSVersion };
Utility::Version minOSVersion2{ in2.MinOSVersion };
if (minOSVersion1 != minOSVersion2)
{
return minOSVersion1 < minOSVersion2;
}
}

return false;
};

Expand All @@ -300,7 +317,7 @@ namespace AppInstaller::Manifest
{
AICLI_LOG(Core, Error, << "Duplicate installer: Type [" << InstallerTypeToString(installer.EffectiveInstallerType()) <<
"], Architecture [" << Utility::ToString(installer.Arch) << "], Locale [" << installer.Locale <<
"], Scope [" << ScopeToString(installer.Scope) << "]");
"], Scope [" << ScopeToString(installer.Scope) << "], MinOSVersion [" << installer.MinOSVersion << "]");

resultErrors.emplace_back(ManifestError::DuplicateInstallerEntry);
duplicateInstallerFound = true;
Expand Down
Loading