diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 1b2ea5c3cb..57b534f83d 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -54,6 +54,7 @@ namespace AppInstaller::CLI::Execution DependencySource, AllowedArchitectures, PortableARPEntry, + AllowUnknownScope, Max }; @@ -222,5 +223,11 @@ namespace AppInstaller::CLI::Execution { using value_t = Registry::Portable::PortableARPEntry; }; + + template <> + struct DataMapping + { + using value_t = bool; + }; } } diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp index 11a8ccbdee..470edb9ef9 100644 --- a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp +++ b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -310,10 +310,10 @@ namespace AppInstaller::CLI::Workflow struct ScopeComparator : public details::ComparisonField { - ScopeComparator(Manifest::ScopeEnum preference, Manifest::ScopeEnum requirement) : - details::ComparisonField("Scope"), m_preference(preference), m_requirement(requirement) {} + ScopeComparator(Manifest::ScopeEnum preference, Manifest::ScopeEnum requirement, bool allowUnknownInAdditionToRequired) : + details::ComparisonField("Scope"), m_preference(preference), m_requirement(requirement), m_allowUnknownInAdditionToRequired(allowUnknownInAdditionToRequired) {} - static std::unique_ptr Create(const Execution::Args& args) + static std::unique_ptr Create(const Execution::Context& context) { // Preference will always come from settings Manifest::ScopeEnum preference = ConvertScope(Settings::User().Get()); @@ -321,6 +321,7 @@ namespace AppInstaller::CLI::Workflow // Requirement may come from args or settings; args overrides settings. Manifest::ScopeEnum requirement = Manifest::ScopeEnum::Unknown; + const auto& args = context.Args; if (args.Contains(Execution::Args::Type::InstallScope)) { requirement = Manifest::ConvertToScopeEnum(args.GetArg(Execution::Args::Type::InstallScope)); @@ -330,9 +331,21 @@ namespace AppInstaller::CLI::Workflow requirement = ConvertScope(Settings::User().Get()); } + bool allowUnknownInAdditionToRequired = false; + if (context.Contains(Execution::Data::AllowUnknownScope)) + { + allowUnknownInAdditionToRequired = context.Get(); + + // Force the required type to be preferred over Unknown + if (requirement != Manifest::ScopeEnum::Unknown) + { + preference = requirement; + } + } + if (preference != Manifest::ScopeEnum::Unknown || requirement != Manifest::ScopeEnum::Unknown) { - return std::make_unique(preference, requirement); + return std::make_unique(preference, requirement, allowUnknownInAdditionToRequired); } else { @@ -342,7 +355,15 @@ namespace AppInstaller::CLI::Workflow InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override { - if (m_requirement == Manifest::ScopeEnum::Unknown || installer.Scope == m_requirement || DoesInstallerIgnoreScopeFromManifest(installer)) + // Applicable if one of: + // 1. No requirement (aka is Unknown) + // 2. Requirement met + // 3. Installer scope is Unknown and this has been explicitly allowed + // 4. The installer type is scope agnostic (we can control it) + if (m_requirement == Manifest::ScopeEnum::Unknown || + installer.Scope == m_requirement || + (installer.Scope == Manifest::ScopeEnum::Unknown && m_allowUnknownInAdditionToRequired) || + DoesInstallerIgnoreScopeFromManifest(installer)) { return InapplicabilityFlags::None; } @@ -379,6 +400,7 @@ namespace AppInstaller::CLI::Workflow Manifest::ScopeEnum m_preference; Manifest::ScopeEnum m_requirement; + bool m_allowUnknownInAdditionToRequired; }; struct InstalledLocaleComparator : public details::ComparisonField @@ -607,7 +629,7 @@ namespace AppInstaller::CLI::Workflow AddComparator(LocaleComparator::Create(context.Args)); } - AddComparator(ScopeComparator::Create(context.Args)); + AddComparator(ScopeComparator::Create(context)); AddComparator(MachineArchitectureComparator::Create(context, installationMetadata)); } diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs index c032fd12ca..23aa75e842 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -346,6 +346,46 @@ public async Task InstallPortableFailsWithCleanup() Assert.AreEqual(InstallResultStatus.InstallError, installResult.Status); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); Directory.Delete(conflictDirectory, true); + } + + + [Test] + public async Task InstallRequireUserScope() + { + // Find package + var searchResult = FindOnePackage(testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + + // Configure installation + var installOptions = TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + installOptions.PreferredInstallLocation = installDir; + installOptions.PackageInstallScope = PackageInstallScope.User; + + // Install + var installResult = await packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + + // Assert + Assert.AreEqual(InstallResultStatus.NoApplicableInstallers, installResult.Status); + } + + + [Test] + public async Task InstallRequireUserScopeAndUnknown() + { + // Find package + var searchResult = FindOnePackage(testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + + // Configure installation + var installOptions = TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + installOptions.PreferredInstallLocation = installDir; + installOptions.PackageInstallScope = PackageInstallScope.UserOrUnknown; + + // Install + var installResult = await packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + + // Assert + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); } } } \ No newline at end of file diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp index d928d74f5a..d883ee2f82 100644 --- a/src/AppInstallerCLITests/ManifestComparator.cpp +++ b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -640,3 +640,31 @@ TEST_CASE("ManifestComparator_MarketFilter", "[manifest_comparator]") RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::Market}); } } + +TEST_CASE("ManifestComparator_Scope_AllowUnknown", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller expected = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown); + + ManifestComparatorTestContext testContext; + testContext.Args.AddArg(Args::Type::InstallScope, ScopeToString(ScopeEnum::User)); + + SECTION("Default") + { + ManifestComparator mc(testContext, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + REQUIRE(!result); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::Scope }); + } + SECTION("Allow Unknown") + { + testContext.Add(true); + + ManifestComparator mc(testContext, {}); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, expected); + REQUIRE(inapplicabilities.size() == 0); + } +} diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index c45fbd3a1e..d114404c70 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -242,4 +242,23 @@ namespace winrt::Microsoft::Management::Deployment::implementation return {}; } + + std::pair<::AppInstaller::Manifest::ScopeEnum, bool> GetManifestScope(winrt::Microsoft::Management::Deployment::PackageInstallScope scope) + { + switch (scope) + { + case winrt::Microsoft::Management::Deployment::PackageInstallScope::Any: + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::Unknown, false); + case winrt::Microsoft::Management::Deployment::PackageInstallScope::User: + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::User, false); + case winrt::Microsoft::Management::Deployment::PackageInstallScope::System: + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::Machine, false); + case winrt::Microsoft::Management::Deployment::PackageInstallScope::UserOrUnknown: + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::User, true); + case winrt::Microsoft::Management::Deployment::PackageInstallScope::SystemOrUnknown: + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::Machine, true); + } + + return std::make_pair(::AppInstaller::Manifest::ScopeEnum::Unknown, false); + } } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index 1d81c32ee2..4f97f7331a 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -17,6 +17,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::FindPackagesResultStatus FindPackagesResultStatus(winrt::hresult hresult); std::optional<::AppInstaller::Utility::Architecture> GetUtilityArchitecture(winrt::Windows::System::ProcessorArchitecture architecture); std::optional GetWindowsSystemProcessorArchitecture(::AppInstaller::Utility::Architecture architecture); + std::pair<::AppInstaller::Manifest::ScopeEnum, bool> GetManifestScope(winrt::Microsoft::Management::Deployment::PackageInstallScope scope); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_) \ if constexpr (std::is_same_v) \ diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index f9702387ba..6d99b30bf7 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -351,13 +351,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation } // If the PackageInstallScope is anything other than ::Any then set it as a requirement. - if (options.PackageInstallScope() == PackageInstallScope::System) + auto manifestScope = GetManifestScope(options.PackageInstallScope()); + if (manifestScope.first != ::AppInstaller::Manifest::ScopeEnum::Unknown) { - context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(::AppInstaller::Manifest::ScopeEnum::Machine)); - } - else if (options.PackageInstallScope() == PackageInstallScope::User) - { - context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(::AppInstaller::Manifest::ScopeEnum::User)); + context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(manifestScope.first)); + context->Add(manifestScope.second); } if (options.PackageInstallMode() == PackageInstallMode::Interactive) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index bfb32a3ce7..8afa0b19cd 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(4)] + [contractversion(5)] apicontract WindowsPackageManagerContract{}; /// State of the install @@ -551,6 +551,13 @@ namespace Microsoft.Management.Deployment User, /// Only System installers will be valid System, + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 5)] + { + /// Both User and Unknown install scope installers are valid + UserOrUnknown, + /// Both System and Unknown install scope installers are valid + SystemOrUnknown, + } }; [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 1)]