winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 020c2f0f8a759fb87d094a5ab6d98e825b442d2d
parent 128607f43088642d1c1f0b9160bea649011c03ca
Author: Ryan <69221034+ryfu-msft@users.noreply.github.com>
Date:   Wed, 30 Aug 2023 13:21:34 -0700

Add support for `--Installer-Type` argument for commands (#3516)


Diffstat:
Mdoc/Settings.md | 12++++++++++++
Mschemas/JSON/settings/settings.schema.0.2.json | 22++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 1+
Msrc/AppInstallerCLICore/Commands/ShowCommand.cpp | 1+
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 1+
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.cpp | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/AppInstallerCLIE2ETests/Constants.cs | 1+
Msrc/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Interop/InstallInterop.cs | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/ShowCommand.cs | 24++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml | 2++
Msrc/AppInstallerCLITests/ManifestComparator.cpp | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 4++++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 23++++++++++++++++++++++-
Msrc/Microsoft.Management.Deployment/InstallOptions.cpp | 8++++++++
Msrc/Microsoft.Management.Deployment/InstallOptions.h | 3+++
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 6++++++
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 3+++
19 files changed, 408 insertions(+), 49 deletions(-)

diff --git a/doc/Settings.md b/doc/Settings.md @@ -140,6 +140,18 @@ The `architectures` behavior affects what architectures will be selected when in }, ``` +### Installer Types + +The `installerTypes` behavior affects what installer types will be selected when installing a package. The matching parameter is `--installer-type`. + +```json + "installBehavior": { + "preferences": { + "installerTypes": ["msi", "msix"] + } + }, +``` + ### Default install root The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -93,6 +93,28 @@ "minItems": 1, "maxItems": 4 } + }, + "installerTypes": { + "description": "The installerType(s) to use for a package install", + "type": "array", + "items": { + "uniqueItems": "true", + "type": "string", + "enum": [ + "inno", + "wix", + "msi", + "nullsoft", + "zip", + "msix", + "exe", + "burn", + "msstore", + "portable" + ], + "minItems": 1, + "maxItems": 9 + } } } }, diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -30,6 +30,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Source), Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::InstallArchitecture), + Argument::ForType(Args::Type::InstallerType), Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -27,6 +27,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Exact), Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Execution::Args::Type::InstallArchitecture), + Argument::ForType(Execution::Args::Type::InstallerType), Argument::ForType(Execution::Args::Type::Locale), Argument::ForType(Execution::Args::Type::ListVersions), Argument::ForType(Execution::Args::Type::CustomHeader), diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -57,6 +57,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::InstallLocation), // -l Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::InstallArchitecture), // -a + Argument::ForType(Args::Type::InstallerType), Argument::ForType(Args::Type::Locale), Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::SkipDependencies), diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -247,26 +247,34 @@ namespace AppInstaller::CLI::Workflow struct InstallerTypeComparator : public details::ComparisonField { - InstallerTypeComparator(std::vector<InstallerTypeEnum> requirement) : - details::ComparisonField("Installer Type"), m_requirement(std::move(requirement)) + InstallerTypeComparator(std::vector<InstallerTypeEnum> preference, std::vector<InstallerTypeEnum> requirement) : + details::ComparisonField("Installer Type"), m_preference(std::move(preference)), m_requirement(std::move(requirement)) { + m_preferenceAsString = Utility::ConvertContainerToString(m_preference, InstallerTypeToString); m_requirementAsString = Utility::ConvertContainerToString(m_requirement, InstallerTypeToString); AICLI_LOG(CLI, Verbose, - << "InstallerType Comparator created with Required InstallerTypes: " << m_requirementAsString); + << "InstallerType Comparator created with Required InstallerTypes: " << m_requirementAsString + << " , Preferred InstallerTypes: " << m_preferenceAsString); } static std::unique_ptr<InstallerTypeComparator> Create(const Execution::Args& args) { + std::vector<InstallerTypeEnum> preference; std::vector<InstallerTypeEnum> requirement; if (args.Contains(Execution::Args::Type::InstallerType)) { requirement.emplace_back(Manifest::ConvertToInstallerTypeEnum(std::string(args.GetArg(Execution::Args::Type::InstallerType)))); } + else + { + preference = Settings::User().Get<Settings::Setting::InstallerTypePreference>(); + requirement = Settings::User().Get<Settings::Setting::InstallerTypeRequirement>(); + } - if (!requirement.empty()) + if (!preference.empty() || !requirement.empty()) { - return std::make_unique<InstallerTypeComparator>(requirement); + return std::make_unique<InstallerTypeComparator>(preference, requirement); } else { @@ -274,17 +282,24 @@ namespace AppInstaller::CLI::Workflow } } + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override + { + std::string result = "InstallerType ["; + result += InstallerTypeToString(installer.EffectiveInstallerType()); + result += "] does not match required InstallerTypes: "; + result += m_requirementAsString; + return result; + } + InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override { if (!m_requirement.empty()) { - for (auto requiredInstallerType : m_requirement) + // The installer is applicable if the effective or base installer type matches. + if (ContainsInstallerType(m_requirement, installer.EffectiveInstallerType()) || + ContainsInstallerType(m_requirement, installer.BaseInstallerType)) { - // The installer is applicable if the installer type or nested installer type matches. (User should be allowed to specify 'zip') - if (installer.EffectiveInstallerType() == requiredInstallerType || installer.BaseInstallerType == requiredInstallerType) - { - return InapplicabilityFlags::None; - } + return InapplicabilityFlags::None; } return InapplicabilityFlags::InstallerType; @@ -295,27 +310,46 @@ namespace AppInstaller::CLI::Workflow } } - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "InstallerType does not match required type: "; - result += InstallerTypeToString(installer.EffectiveInstallerType()); - result += "Required InstallerTypes: "; - result += m_requirementAsString; - return result; - } - bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override { - // TODO: Current implementation assumes there is only a single installer type requirement. This needs to be updated - // once multiple installerType requirements and preferences are accepted. - UNREFERENCED_PARAMETER(first); - UNREFERENCED_PARAMETER(second); - return true; + if (m_preference.empty()) + { + return false; + } + + for (Manifest::InstallerTypeEnum installerTypePreference : m_preference) + { + bool isFirstInstallerTypePreferred = + first.EffectiveInstallerType() == installerTypePreference || + first.BaseInstallerType == installerTypePreference; + + bool isSecondInstallerTypePreferred = + second.EffectiveInstallerType() == installerTypePreference || + second.BaseInstallerType == installerTypePreference; + + if (isFirstInstallerTypePreferred && isSecondInstallerTypePreferred) + { + return false; + } + else if (isFirstInstallerTypePreferred != isSecondInstallerTypePreferred) + { + return isFirstInstallerTypePreferred; + } + } + + return false; } private: + std::vector<InstallerTypeEnum> m_preference; std::vector<InstallerTypeEnum> m_requirement; + std::string m_preferenceAsString; std::string m_requirementAsString; + + bool ContainsInstallerType(const std::vector<InstallerTypeEnum>& selection, InstallerTypeEnum installerType) + { + return std::find(selection.begin(), selection.end(), installerType) != selection.end(); + } }; struct InstalledTypeComparator : public details::ComparisonField @@ -728,11 +762,11 @@ namespace AppInstaller::CLI::Workflow // Filter order is not important, but comparison order determines priority. // TODO: There are improvements to be made here around ordering, especially in the context of implicit vs explicit vs command line preferences. - AddComparator(InstallerTypeComparator::Create(context.Args)); AddComparator(InstalledTypeComparator::Create(installationMetadata)); AddComparator(LocaleComparator::Create(context.Args, installationMetadata)); AddComparator(ScopeComparator::Create(context)); AddComparator(MachineArchitectureComparator::Create(context, installationMetadata)); + AddComparator(InstallerTypeComparator::Create(context.Args)); } InstallerAndInapplicabilities ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -117,6 +117,7 @@ namespace AppInstallerCLIE2ETests public const string PortablePackageUserRoot = "portablePackageUserRoot"; public const string PortablePackageMachineRoot = "portablePackageMachineRoot"; public const string InstallBehaviorScope = "scope"; + public const string InstallerTypes = "installerTypes"; // Configuration public const string PSGalleryName = "PSGallery"; diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -92,13 +92,7 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="status">Status.</param> public static void ConfigureFeature(string featureName, bool status) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); - - if (!settingsJson.ContainsKey("experimentalFeatures")) - { - settingsJson["experimentalFeatures"] = new JObject(); - } - + JObject settingsJson = GetJsonSettingsObject("experimentalFeatures"); var experimentalFeatures = settingsJson["experimentalFeatures"]; experimentalFeatures[featureName] = status; @@ -112,13 +106,7 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="value">Setting value.</param> public static void ConfigureInstallBehavior(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); - - if (!settingsJson.ContainsKey("installBehavior")) - { - settingsJson["installBehavior"] = new JObject(); - } - + JObject settingsJson = GetJsonSettingsObject("installBehavior"); var installBehavior = settingsJson["installBehavior"]; installBehavior[settingName] = value; @@ -132,13 +120,28 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="value">Setting value.</param> public static void ConfigureInstallBehaviorPreferences(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); + JObject settingsJson = GetJsonSettingsObject("installBehavior"); + var installBehavior = settingsJson["installBehavior"]; - if (!settingsJson.ContainsKey("installBehavior")) + if (installBehavior["preferences"] == null) { - settingsJson["installBehavior"] = new JObject(); + installBehavior["preferences"] = new JObject(); } + var preferences = installBehavior["preferences"]; + preferences[settingName] = value; + + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); + } + + /// <summary> + /// Configure the install behavior preferences. + /// </summary> + /// <param name="settingName">Setting name.</param> + /// <param name="value">Setting value array.</param> + public static void ConfigureInstallBehaviorPreferences(string settingName, string[] value) + { + JObject settingsJson = GetJsonSettingsObject("installBehavior"); var installBehavior = settingsJson["installBehavior"]; if (installBehavior["preferences"] == null) @@ -147,7 +150,7 @@ namespace AppInstallerCLIE2ETests.Helpers } var preferences = installBehavior["preferences"]; - preferences[settingName] = value; + preferences[settingName] = new JArray(value); File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } @@ -159,13 +162,28 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="value">Setting value.</param> public static void ConfigureInstallBehaviorRequirements(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); + JObject settingsJson = GetJsonSettingsObject("installBehavior"); + var installBehavior = settingsJson["installBehavior"]; - if (!settingsJson.ContainsKey("installBehavior")) + if (installBehavior["requirements"] == null) { - settingsJson["installBehavior"] = new JObject(); + installBehavior["requirements"] = new JObject(); } + var requirements = installBehavior["requirements"]; + requirements[settingName] = value; + + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); + } + + /// <summary> + /// Configure the install behavior requirements. + /// </summary> + /// <param name="settingName">Setting name.</param> + /// <param name="value">Setting value array.</param> + public static void ConfigureInstallBehaviorRequirements(string settingName, string[] value) + { + JObject settingsJson = GetJsonSettingsObject("installBehavior"); var installBehavior = settingsJson["installBehavior"]; if (installBehavior["requirements"] == null) @@ -174,7 +192,7 @@ namespace AppInstallerCLIE2ETests.Helpers } var requirements = installBehavior["requirements"]; - requirements[settingName] = value; + requirements[settingName] = new JArray(value); File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } @@ -191,5 +209,17 @@ namespace AppInstallerCLIE2ETests.Helpers ConfigureFeature("pinning", status); ConfigureFeature("configuration", status); } + + private static JObject GetJsonSettingsObject(string objectName) + { + JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); + + if (!settingsJson.ContainsKey(objectName)) + { + settingsJson[objectName] = new JObject(); + } + + return settingsJson; + } } } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using System; using System.IO; using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; @@ -664,6 +665,58 @@ namespace AppInstallerCLIE2ETests } /// <summary> + /// Test install a package using a specific installer type. + /// </summary> + [Test] + public void InstallWithInstallerTypeArgument() + { + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMultipleInstallers --silent -l {installDir} --installer-type exe"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); + } + + /// <summary> + /// Test install package with installer type preference settings. + /// </summary> + [Test] + public void InstallWithInstallerTypePreference() + { + string[] installerTypePreference = { "nullsoft" }; + WinGetSettingsHelper.ConfigureInstallBehaviorPreferences(Constants.InstallerTypes, installerTypePreference); + + string installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMultipleInstallers --silent -l {installDir}"); + + // Reset installer type preferences. + WinGetSettingsHelper.ConfigureInstallBehaviorPreferences(Constants.InstallerTypes, Array.Empty<string>()); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir), "/S"); + } + + /// <summary> + /// Test install package with installer type requirement settings. + /// </summary> + [Test] + public void InstallWithInstallerTypeRequirement() + { + string[] installerTypeRequirement = { "inno" }; + WinGetSettingsHelper.ConfigureInstallBehaviorRequirements(Constants.InstallerTypes, installerTypeRequirement); + + string installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMultipleInstallers --silent -l {installDir}"); + + // Reset installer type requirements. + WinGetSettingsHelper.ConfigureInstallBehaviorRequirements(Constants.InstallerTypes, Array.Empty<string>()); + + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICABLE_INSTALLER, result.ExitCode); + Assert.True(result.StdOut.Contains("No applicable installer found; see logs for more details.")); + } + + /// <summary> /// This test flow is intended to test an EXE that actually installs an MSIX internally, and whose name+publisher /// information resembles an existing installation. Given this, the goal is to get correlation to stick to the /// MSIX rather than the ARP entry that we would match with in the absence of the package family name being present. diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -598,6 +598,31 @@ namespace AppInstallerCLIE2ETests.Interop } /// <summary> + /// Test installing a package with a specific installer type install option. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task InstallWithInstallerType() + { + // Find package + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + installOptions.PreferredInstallLocation = this.installDir; + installOptions.InstallerType = PackageInstallerType.Msi; + installOptions.AcceptPackageAgreements = true; + + // Install + var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + + // Assert + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + Assert.True(TestCommon.VerifyTestMsiInstalledAndCleanup(this.installDir)); + } + + /// <summary> /// Test to verify the GetApplicableInstaller() COM call returns the correct manifest installer metadata. /// </summary> [Test] diff --git a/src/AppInstallerCLIE2ETests/ShowCommand.cs b/src/AppInstallerCLIE2ETests/ShowCommand.cs @@ -124,5 +124,29 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("No package found matching input criteria.")); } + + /// <summary> + /// Test show with installer type. + /// </summary> + [Test] + public void ShowWithInstallerTypeArg() + { + var result = TestCommon.RunAICLICommand("show", $"--id AppInstallerTest.TestMultipleInstallers --installer-type msi"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Found TestMultipleInstallers [AppInstallerTest.TestMultipleInstallers]")); + Assert.True(result.StdOut.Contains("Installer Type: msi")); + } + + /// <summary> + /// Test show with an archive installer type. + /// </summary> + [Test] + public void ShowWithZipInstallerTypeArg() + { + var result = TestCommon.RunAICLICommand("show", $"--id AppInstallerTest.TestMultipleInstallers --installer-type zip"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Found TestMultipleInstallers [AppInstallerTest.TestMultipleInstallers]")); + Assert.True(result.StdOut.Contains("Installer Type: exe (zip)")); + } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml @@ -11,6 +11,8 @@ Installers: InstallerType: nullsoft InstallerSha256: <EXEHASH> Scope: user + InstallerSwitches: + InstallLocation: /InstallDir <INSTALLPATH> - Architecture: x86 InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi InstallerSha256: <MSIHASH> diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -738,3 +738,111 @@ TEST_CASE("ManifestComparator_Scope_AllowUnknown", "[manifest_comparator]") REQUIRE(inapplicabilities.size() == 0); } } + +TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller msi = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::User); + ManifestInstaller exe = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::User); + ManifestInstaller msix = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msix, ScopeEnum::User); + + SECTION("Msi arg requirement") + { + ManifestComparatorTestContext context; + context.Args.AddArg(Args::Type::InstallerType, "msi"s); + + ManifestComparator mc(context, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msi); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); + } + SECTION("Msix arg requirement") + { + ManifestComparatorTestContext context; + context.Args.AddArg(Args::Type::InstallerType, "msix"s); + + ManifestComparator mc(context, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); + } + SECTION("Portable arg requirement") + { + ManifestComparatorTestContext context; + context.Args.AddArg(Args::Type::InstallerType, "portable"s); + + ManifestComparator mc(context, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + REQUIRE(!result); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); + } + SECTION("Exe preference") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Exe }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("Preference does not exist") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Portable }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msi); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("Multiple preferences") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("Multiple preferences alternate order") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("Exe requirement") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Exe }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); + } + SECTION("Inno requirement") + { + TestUserSettings settings; + settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Inno }); + + ManifestComparator mc(ManifestComparatorTestContext{}, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + REQUIRE(!result); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); + } +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -80,6 +80,8 @@ namespace AppInstaller::Settings InstallArchitectureRequirement, InstallLocalePreference, InstallLocaleRequirement, + InstallerTypePreference, + InstallerTypeRequirement, InstallDefaultRoot, InstallSkipDependencies, DisableInstallNotes, @@ -154,6 +156,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, Manifest::ScopeEnum, Manifest::ScopeEnum::Unknown, ".installBehavior.requirements.scope"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocalePreference, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.preferences.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocaleRequirement, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.requirements.locale"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallerTypePreference, std::vector<std::string>, std::vector<Manifest::InstallerTypeEnum>, {}, ".installBehavior.preferences.installerTypes"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallerTypeRequirement, std::vector<std::string>, std::vector<Manifest::InstallerTypeEnum>, {}, ".installBehavior.requirements.installerTypes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallSkipDependencies, bool, bool, false, ".installBehavior.skipDependencies"sv); SETTINGMAPPING_SPECIALIZATION(Setting::DisableInstallNotes, bool, bool, false, ".installBehavior.disableInstallNotes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageUserRoot"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -286,7 +286,8 @@ namespace AppInstaller::Settings WINGET_VALIDATE_SIGNATURE(InstallArchitecturePreference) { std::vector<Utility::Architecture> archs; - for (auto const& i : value) { + for (auto const& i : value) + { Utility::Architecture arch = Utility::ConvertToArchitectureEnum(i); if (Utility::IsApplicableArchitecture(arch) == Utility::InapplicableArchitecture) { @@ -342,6 +343,26 @@ namespace AppInstaller::Settings return SettingMapping<Setting::InstallLocalePreference>::Validate(value); } + WINGET_VALIDATE_SIGNATURE(InstallerTypePreference) + { + std::vector<Manifest::InstallerTypeEnum> installerTypes; + for (auto const& i : value) + { + Manifest::InstallerTypeEnum installerType = Manifest::ConvertToInstallerTypeEnum(i); + if (installerType == Manifest::InstallerTypeEnum::Unknown) + { + return {}; + } + installerTypes.emplace_back(installerType); + } + return installerTypes; + } + + WINGET_VALIDATE_SIGNATURE(InstallerTypeRequirement) + { + return SettingMapping<Setting::InstallerTypePreference>::Validate(value); + } + WINGET_VALIDATE_SIGNATURE(InstallDefaultRoot) { return ValidatePathValue(value); diff --git a/src/Microsoft.Management.Deployment/InstallOptions.cpp b/src/Microsoft.Management.Deployment/InstallOptions.cpp @@ -60,6 +60,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation { m_packageInstallMode = value; } + winrt::Microsoft::Management::Deployment::PackageInstallerType InstallOptions::InstallerType() + { + return m_installerType; + } + void InstallOptions::InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value) + { + m_installerType = value; + } hstring InstallOptions::LogOutputPath() { return hstring(m_logOutputPath); diff --git a/src/Microsoft.Management.Deployment/InstallOptions.h b/src/Microsoft.Management.Deployment/InstallOptions.h @@ -19,6 +19,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation void PackageInstallScope(winrt::Microsoft::Management::Deployment::PackageInstallScope const& value); winrt::Microsoft::Management::Deployment::PackageInstallMode PackageInstallMode(); void PackageInstallMode(winrt::Microsoft::Management::Deployment::PackageInstallMode const& value); + winrt::Microsoft::Management::Deployment::PackageInstallerType InstallerType(); + void InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value); hstring LogOutputPath(); void LogOutputPath(hstring const& value); bool AllowHashMismatch(); @@ -49,6 +51,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::wstring m_preferredInstallLocation = L""; winrt::Microsoft::Management::Deployment::PackageInstallScope m_packageInstallScope = winrt::Microsoft::Management::Deployment::PackageInstallScope::Any; winrt::Microsoft::Management::Deployment::PackageInstallMode m_packageInstallMode = winrt::Microsoft::Management::Deployment::PackageInstallMode::Default; + winrt::Microsoft::Management::Deployment::PackageInstallerType m_installerType = winrt::Microsoft::Management::Deployment::PackageInstallerType::Unknown; std::wstring m_logOutputPath = L""; bool m_allowHashMismatch = false; bool m_bypassIsStoreClientBlockedPolicyCheck = false; diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -386,6 +386,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation context->Args.AddArg(Execution::Args::Type::Silent); } + auto installerType = GetManifestInstallerType(options.InstallerType()); + if (installerType != AppInstaller::Manifest::InstallerTypeEnum::Unknown) + { + context->Args.AddArg(Execution::Args::Type::InstallerType, AppInstaller::Manifest::InstallerTypeToString(installerType)); + } + if (!options.PreferredInstallLocation().empty()) { context->Args.AddArg(Execution::Args::Type::InstallLocation, ::AppInstaller::Utility::ConvertToUTF8(options.PreferredInstallLocation())); diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -889,6 +889,9 @@ namespace Microsoft.Management.Deployment { // Skip installing the dependencies for the package. Boolean SkipDependencies; + + /// The package installer type. + PackageInstallerType InstallerType; } }