winget-cli

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

commit 3556a3ed5992a5b4e6a9f9dbb0816dc33c2154a7
parent 990e1a64bf6dab469f6411a0fe266593f6704faa
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Wed, 23 Apr 2025 16:27:25 -0700

Dscv3 package resource (#5395)

## Change
Add a new DSC v3 resource: `package`. This largely follows the design of
the v2 resource, but uses the v3 paradigm for existence.

Other minor changes:
- Adds a `StdErrLogger` and attaches it during resource execution. This
currently writes all logs at `Error` or higher to `std::cerr`, allowing
`dsc.exe` to receive (and pass on) failure information.
- Uses the type of the standard input handle to determine if it should
be read from (will not read JSON input from the interactive console,
only piped input).
- Adds support for localized schema generation for resources.
- Moves `ManifestComparator` to common for re-use.

## Validation
Added E2E tests to invoke the package resource in a variety of
situations.
Diffstat:
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 4++--
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 12++++++------
Msrc/AppInstallerCLICore/Argument.cpp | 5+++++
Msrc/AppInstallerCLICore/Commands/DscCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/DscCommandBase.cpp | 33++++++++++++++++++++++++---------
Msrc/AppInstallerCLICore/Commands/DscCommandBase.h | 2+-
Msrc/AppInstallerCLICore/Commands/DscComposableObject.cpp | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCLICore/Commands/DscComposableObject.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Asrc/AppInstallerCLICore/Commands/DscPackageResource.cpp | 527+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/DscPackageResource.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/DscTestFileResource.cpp | 18+++++++++---------
Msrc/AppInstallerCLICore/Commands/DscTestJsonResource.cpp | 18+++++++++---------
Msrc/AppInstallerCLICore/Core.cpp | 2+-
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/ExecutionContext.h | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 34++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionReporter.h | 6++++++
Msrc/AppInstallerCLICore/Resources.h | 13+++++++++++++
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp | 7+++----
Dsrc/AppInstallerCLICore/Workflows/ManifestComparator.cpp | 962-------------------------------------------------------------------------------
Dsrc/AppInstallerCLICore/Workflows/ManifestComparator.h | 112-------------------------------------------------------------------------------
Msrc/AppInstallerCLICore/Workflows/RepairFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/ShowFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 8++++----
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.h | 8++++----
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 4++++
Asrc/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs | 677+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/DownloadCommand.cs | 4++--
Msrc/AppInstallerCLIE2ETests/Helpers/TestCommon.cs | 15+++++++++++++--
Msrc/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs | 20++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs | 4++--
Msrc/AppInstallerCLIE2ETests/Interop/InstallInterop.cs | 7+++----
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml | 15+++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml | 15+++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml | 15+++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml | 15+++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_NoScope.yaml | 20++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 39+++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 9+++++----
Msrc/AppInstallerCLITests/ManifestComparator.cpp | 117++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 4++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 12++++++++++++
Asrc/AppInstallerCommonCore/Manifest/ManifestComparator.cpp | 897+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Public/winget/ManifestComparator.h | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Public/winget/StdErrLogger.h | 31+++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/StdErrLogger.cpp | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/PackageVersionSelection.cpp | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h | 13+++++++++++++
Msrc/AppInstallerTestExeInstaller/main.cpp | 2+-
Msrc/Microsoft.Management.Deployment/CatalogPackage.cpp | 54+++++-------------------------------------------------
Msrc/Microsoft.Management.Deployment/PackageVersionInfo.cpp | 48+++++++++++++++++++++++++++++++++++++++---------
53 files changed, 3057 insertions(+), 1289 deletions(-)

diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -305,6 +305,7 @@ <ClInclude Include="Commands\DscCommand.h" /> <ClInclude Include="Commands\DscCommandBase.h" /> <ClInclude Include="Commands\DscComposableObject.h" /> + <ClInclude Include="Commands\DscPackageResource.h" /> <ClInclude Include="Commands\DscTestFileResource.h" /> <ClInclude Include="Commands\DscTestJsonResource.h" /> <ClInclude Include="Commands\ErrorCommand.h" /> @@ -369,7 +370,6 @@ <ClInclude Include="Workflows\SettingsFlow.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> - <ClInclude Include="Workflows\ManifestComparator.h" /> <ClInclude Include="Workflows\ResumeFlow.h" /> <ClInclude Include="Workflows\ShowFlow.h" /> <ClInclude Include="Workflows\SourceFlow.h" /> @@ -391,6 +391,7 @@ <ClCompile Include="Commands\DscCommand.cpp" /> <ClCompile Include="Commands\DscCommandBase.cpp" /> <ClCompile Include="Commands\DscComposableObject.cpp" /> + <ClCompile Include="Commands\DscPackageResource.cpp" /> <ClCompile Include="Commands\DscTestFileResource.cpp" /> <ClCompile Include="Commands\DscTestJsonResource.cpp" /> <ClCompile Include="Commands\ErrorCommand.cpp" /> @@ -456,7 +457,6 @@ <ClCompile Include="Workflows\SettingsFlow.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> - <ClCompile Include="Workflows\ManifestComparator.cpp" /> <ClCompile Include="Workflows\ResumeFlow.cpp" /> <ClCompile Include="Workflows\ShowFlow.cpp" /> <ClCompile Include="Workflows\SourceFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -47,9 +47,6 @@ <ClInclude Include="Workflows\InstallFlow.h"> <Filter>Workflows</Filter> </ClInclude> - <ClInclude Include="Workflows\ManifestComparator.h"> - <Filter>Workflows</Filter> - </ClInclude> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h"> <Filter>Workflows</Filter> </ClInclude> @@ -284,6 +281,9 @@ <ClInclude Include="Commands\DscTestJsonResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Commands\DscPackageResource.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -301,9 +301,6 @@ <ClCompile Include="Core.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="Workflows\ManifestComparator.cpp"> - <Filter>Workflows</Filter> - </ClCompile> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp"> <Filter>Workflows</Filter> </ClCompile> @@ -535,6 +532,9 @@ <ClCompile Include="Commands\DscTestJsonResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="Commands\DscPackageResource.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -289,6 +289,8 @@ namespace AppInstaller::CLI return { type, "force"_liv, ArgTypeCategory::CopyFlagToSubContext }; case Execution::Args::Type::OutputFile: return { type, "output"_liv, 'o' }; + case Execution::Args::Type::Correlation: + return { type, "correlation"_liv }; case Execution::Args::Type::DependencySource: return { type, "dependency-source"_liv, ArgTypeCategory::ExtendedSource }; @@ -468,6 +470,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::NoProxyArgumentDescription, ArgumentType::Flag, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions }; case Args::Type::Family: return Argument{ type, Resource::String::FontFamilyNameArgumentDescription, ArgumentType::Positional, false }; + case Args::Type::Correlation: + return Argument{ type, Resource::String::CorrelationArgumentDescription, ArgumentType::Standard, Argument::Visibility::Hidden }; default: THROW_HR(E_UNEXPECTED); } @@ -486,6 +490,7 @@ namespace AppInstaller::CLI args.emplace_back(Args::Type::DisableInteractivity, Resource::String::DisableInteractivityArgumentDescription, ArgumentType::Flag, false); args.push_back(ForType(Args::Type::Proxy)); args.push_back(ForType(Args::Type::NoProxy)); + args.push_back(ForType(Args::Type::Correlation)); } std::string Argument::GetUsageString() const diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "DscCommand.h" +#include "DscPackageResource.h" #ifndef AICLI_DISABLE_TEST_HOOKS #include "DscTestFileResource.h" @@ -13,6 +14,7 @@ namespace AppInstaller::CLI std::vector<std::unique_ptr<Command>> DscCommand::GetCommands() const { return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ + std::make_unique<DscPackageResource>(FullName()), #ifndef AICLI_DISABLE_TEST_HOOKS std::make_unique<DscTestFileResource>(FullName()), std::make_unique<DscTestJsonResource>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/DscCommandBase.cpp b/src/AppInstallerCLICore/Commands/DscCommandBase.cpp @@ -4,6 +4,7 @@ #include "DscCommandBase.h" #include "DscCommand.h" #include <winget/Runtime.h> +#include <winget/StdErrLogger.h> #define WINGET_DSC_FUNCTION_FOREACH(_macro_) \ _macro_(Get); \ @@ -201,8 +202,7 @@ namespace AppInstaller::CLI void DscCommandBase::ExecuteInternal(Execution::Context& context) const { context.Reporter.SetChannel(Execution::Reporter::Channel::Json); - - // TODO: Consider adding a stderr logger + Logging::StdErrLogger::Add(); #define WINGET_DSC_FUNCTION_ARGUMENT(_function_) \ if (context.Args.Contains(Execution::Args::Type::DscResourceFunction ## _function_)) \ @@ -266,18 +266,33 @@ namespace AppInstaller::CLI #undef WINGET_DSC_FUNCTION_METHOD - std::optional<Json::Value> DscCommandBase::GetJsonFromInput(Execution::Context& context) const + std::optional<Json::Value> DscCommandBase::GetJsonFromInput(Execution::Context& context, bool terminateContextOnError) const { - Json::Value result; - Json::CharReaderBuilder builder; - Json::String errors; - if (!Json::parseFromStream(builder, context.Reporter.RawInputStream(), &result, &errors)) + // Don't attempt to read from an interactive stream as this will just block + if (!context.Reporter.InputStreamIsInteractive()) { + AICLI_LOG(CLI, Verbose, << "Reading Json from input stream..."); + + Json::Value result; + Json::CharReaderBuilder builder; + Json::String errors; + if (Json::parseFromStream(builder, context.Reporter.RawInputStream(), &result, &errors)) + { + AICLI_LOG(CLI, Info, << "Json from input stream:\n" << Json::writeString(Json::StreamWriterBuilder{}, result)); + return result; + } + AICLI_LOG(CLI, Error, << "Failed to read input JSON: " << errors); - AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, std::nullopt); } - return result; + if (terminateContextOnError) + { + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, std::nullopt); + } + else + { + return std::nullopt; + } } void DscCommandBase::WriteJsonOutputLine(Execution::Context& context, const Json::Value& value) const diff --git a/src/AppInstallerCLICore/Commands/DscCommandBase.h b/src/AppInstallerCLICore/Commands/DscCommandBase.h @@ -94,7 +94,7 @@ namespace AppInstaller::CLI virtual void ResourceFunctionManifest(Execution::Context& context) const; // Parses a JSON object from stdin. - std::optional<Json::Value> GetJsonFromInput(Execution::Context& context) const; + std::optional<Json::Value> GetJsonFromInput(Execution::Context& context, bool terminateContextOnError = true) const; // Writes the value to the context output. void WriteJsonOutputLine(Execution::Context& context, const Json::Value& value) const; diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.cpp b/src/AppInstallerCLICore/Commands/DscComposableObject.cpp @@ -5,6 +5,32 @@ namespace AppInstaller::CLI { + namespace + { + std::string GetTypeString(Json::ValueType type) + { + switch (type) + { + case Json::nullValue: + return "null"; + case Json::intValue: + case Json::uintValue: + case Json::realValue: + return "number"; + case Json::stringValue: + return "string"; + case Json::booleanValue: + return "boolean"; + case Json::arrayValue: + return "array"; + case Json::objectValue: + return "object"; + default: + THROW_HR(E_UNEXPECTED); + } + } + } + namespace details { const Json::Value* GetProperty(const Json::Value& object, std::string_view name) @@ -32,7 +58,14 @@ namespace AppInstaller::CLI return result; } - void AddPropertySchema(Json::Value& object, std::string_view name, DscComposablePropertyFlag flags, std::string_view type, std::string_view description) + void AddPropertySchema( + Json::Value& object, + std::string_view name, + DscComposablePropertyFlag flags, + Json::ValueType type, + std::string_view description, + const std::vector<std::string>& enumValues, + const std::optional<std::string>& defaultValue) { Json::Value& propertiesObject = object["properties"]; @@ -45,13 +78,30 @@ namespace AppInstaller::CLI Json::Value property{ Json::ValueType::objectValue }; - if (!type.empty()) + if (type != Json::ValueType::objectValue) { - property["type"] = std::string{ type }; + property["type"] = GetTypeString(type); } property["description"] = std::string{ description }; + if (!enumValues.empty()) + { + Json::Value enumArray{ Json::ValueType::arrayValue }; + + for (const std::string& enumValue : enumValues) + { + enumArray.append(enumValue); + } + + property["enum"] = std::move(enumArray); + } + + if (defaultValue) + { + property["default"] = defaultValue.value(); + } + propertiesObject[nameString] = std::move(property); if (WI_IsFlagSet(flags, DscComposablePropertyFlag::Required)) diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h @@ -4,10 +4,15 @@ #include <AppInstallerErrors.h> #include <AppInstallerLanguageUtilities.h> #include <AppInstallerLogging.h> +#include <winget/LocIndependent.h> +#include "Resources.h" #include <json/json.h> #include <optional> +#include <string> +#include <vector> using namespace std::string_view_literals; +using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI { @@ -33,7 +38,14 @@ namespace AppInstaller::CLI Json::Value GetBaseSchema(const std::string& title); // Adds a property to the schema object. - void AddPropertySchema(Json::Value& object, std::string_view name, DscComposablePropertyFlag flags, std::string_view type, std::string_view description); + void AddPropertySchema( + Json::Value& object, + std::string_view name, + DscComposablePropertyFlag flags, + Json::ValueType type, + std::string_view description, + const std::vector<std::string>& enumValues, + const std::optional<std::string>& defaultValue); } template <typename PropertyType> @@ -50,9 +62,9 @@ namespace AppInstaller::CLI return value.asBool(); } - static std::string_view SchemaTypeName() + static Json::ValueType SchemaType() { - return "boolean"sv; + return Json::ValueType::booleanValue; } }; @@ -64,9 +76,9 @@ namespace AppInstaller::CLI return value.asString(); } - static std::string_view SchemaTypeName() + static Json::ValueType SchemaType() { - return "string"sv; + return Json::ValueType::stringValue; } }; @@ -78,10 +90,9 @@ namespace AppInstaller::CLI return value; } - static std::string_view SchemaTypeName() + static Json::ValueType SchemaType() { - // Indicates that the schema should not set a type - return {}; + return Json::ValueType::objectValue; } }; @@ -103,16 +114,19 @@ namespace AppInstaller::CLI { DscComposableObject() = default; - DscComposableObject(const std::optional<Json::Value>& input) + DscComposableObject(const std::optional<Json::Value>& input, bool ignoreFieldRequirements = false) { - THROW_HR_IF(E_POINTER, !input); - FromJson(input.value()); + THROW_HR_IF(E_POINTER, !input && !ignoreFieldRequirements); + if (input) + { + FromJson(input.value(), ignoreFieldRequirements); + } } // Read values for each property - void FromJson(const Json::Value& input) + void FromJson(const Json::Value& input, bool ignoreFieldRequirements = false) { - (FoldHelper{}, ..., Property::FromJson(this, details::GetProperty(input, Property::Name()))); + (FoldHelper{}, ..., Property::FromJson(this, details::GetProperty(input, Property::Name()), ignoreFieldRequirements)); } // Populate JSON object with properties. @@ -124,7 +138,7 @@ namespace AppInstaller::CLI } // Copies the appropriate values to a new object for output. - DscComposableObject CopyForOutput() + DscComposableObject CopyForOutput() const { DscComposableObject result; (FoldHelper{}, ..., Property::CopyForOutput(this, &result)); @@ -135,7 +149,7 @@ namespace AppInstaller::CLI static Json::Value Schema(const std::string& title) { Json::Value result = details::GetBaseSchema(title); - (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue<typename Property::Type>::SchemaTypeName(), Property::Description())); + (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue<typename Property::Type>::SchemaType(), Property::Description(), Property::EnumValues(), Property::Default())); return result; } }; @@ -146,7 +160,7 @@ namespace AppInstaller::CLI using Type = PropertyType; static constexpr DscComposablePropertyFlag Flags = PropertyFlags; - static void FromJson(Derived* self, const Json::Value* value) + static void FromJson(Derived* self, const Json::Value* value, bool ignoreFieldRequirements) { if (value) { @@ -154,7 +168,7 @@ namespace AppInstaller::CLI } else { - if constexpr (WI_IsFlagSet(PropertyFlags, DscComposablePropertyFlag::Required)) + if (!ignoreFieldRequirements && WI_IsFlagSet(PropertyFlags, DscComposablePropertyFlag::Required)) { THROW_HR_MSG(WINGET_CONFIG_ERROR_MISSING_FIELD, "Required property `%hs` not provided.", Derived::Name().data()); } @@ -190,25 +204,39 @@ namespace AppInstaller::CLI std::optional<Type> m_value; }; -#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ struct _property_type_ : public DscComposableProperty<_property_type_, _value_type_, _flags_> \ { \ static std::string_view Name() { return _json_name_; } \ - static std::string_view Description() { return _description_; } \ + static Resource::LocString Description() { return _description_; } \ + static std::vector<std::string> EnumValues() { return std::vector<std::string> _enum_vals_; } \ + static std::optional<std::string> Default() { return _default_; } \ std::optional<Type>& _property_name_() { return m_value; } \ const std::optional<Type>& _property_name_() const { return m_value; } \ void _property_name_(std::optional<Type> value) { m_value = std::move(value); } \ -#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ }; -#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(_property_type_, _value_type_, _property_name_, _json_name_, _description_) WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, DscComposablePropertyFlag::None, _description_) -#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(_property_type_, _value_type_, _property_name_, _json_name_, _description_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, DscComposablePropertyFlag::None, _description_, {}, {}) + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, {}, {}) + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_DEFAULT(_property_type_, _value_type_, _property_name_, _json_name_, _description_, _default_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, DscComposablePropertyFlag::None, _description_, {}, _default_) + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(_property_type_, _value_type_, _property_name_, _json_name_, _description_, _enum_vals_, _default_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, DscComposablePropertyFlag::None, _description_, _enum_vals_, _default_) + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM_FLAGS(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(StandardExistProperty, bool, Exist, "_exist", DscComposablePropertyFlag::None, "Indicates whether an instance should/does exist.") - bool ShouldExist() { return m_value.value_or(true); } + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(StandardExistProperty, bool, Exist, "_exist", DscComposablePropertyFlag::None, Resource::String::DscResourcePropertyDescriptionExist, {}, {}) + bool ShouldExist() const { return m_value.value_or(true); } }; - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(StandardInDesiredStateProperty, bool, InDesiredState, "_inDesiredState", "Indicates whether an instance is in the desired state."); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(StandardInDesiredStateProperty, bool, InDesiredState, "_inDesiredState", Resource::String::DscResourcePropertyDescriptionInDesiredState); } diff --git a/src/AppInstallerCLICore/Commands/DscPackageResource.cpp b/src/AppInstallerCLICore/Commands/DscPackageResource.cpp @@ -0,0 +1,527 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscPackageResource.h" +#include "DscComposableObject.h" +#include "Resources.h" +#include "Workflows/WorkflowBase.h" +#include "Workflows/ConfigurationFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/UninstallFlow.h" +#include "Workflows/UpdateFlow.h" +#include <winget/PackageVersionSelection.h> + +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI +{ + namespace + { + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(IdProperty, std::string, Identifier, "id", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageId); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(SourceProperty, std::string, Source, "source", DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageSource); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(VersionProperty, std::string, Version, "version", Resource::String::DscResourcePropertyDescriptionPackageVersion); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(MatchOptionProperty, std::string, MatchOption, "matchOption", Resource::String::DscResourcePropertyDescriptionPackageMatchOption, ({ "equals", "equalsCaseInsensitive", "startsWithCaseInsensitive", "containsCaseInsensitive" }), "equalsCaseInsensitive"); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_DEFAULT(UseLatestProperty, bool, UseLatest, "useLatest", Resource::String::DscResourcePropertyDescriptionPackageUseLatest, "false"); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(InstallModeProperty, std::string, InstallMode, "installMode", Resource::String::DscResourcePropertyDescriptionPackageInstallMode, ({ "default", "silent", "interactive" }), "silent"); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionPackageAcceptAgreements); + + // TODO: To support Scope on this resource: + // 1. Change the installed source to pull in all package info for both scopes by default + // 2. Change the installed source open in workflows to always open for everything, regardless of scope + // 3. Improve correlation handling if needed for cross-scope package installations + // 4. Update the test EXE installer to handle being installed for both scopes + using PackageResourceObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, IdProperty, SourceProperty, VersionProperty, MatchOptionProperty, UseLatestProperty, InstallModeProperty, AcceptAgreementsProperty>; + + std::optional<MatchType> ToMatchType(const std::optional<std::string>& value) + { + if (!value) + { + return std::nullopt; + } + + std::string lowerValue = Utility::ToLower(value.value()); + + if (lowerValue == "equals") + { + return MatchType::Exact; + } + else if (lowerValue == "equals""case""insensitive") + { + return MatchType::CaseInsensitive; + } + else if (lowerValue == "starts""with""case""insensitive") + { + return MatchType::StartsWith; + } + else if (lowerValue == "contains""case""insensitive") + { + return MatchType::Substring; + } + + THROW_HR(E_INVALIDARG); + } + + struct PackageFunctionData + { + PackageFunctionData(Execution::Context& context, const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) : + Input(json, ignoreFieldRequirements), + ParentContext(context) + { + Reset(); + } + + const PackageResourceObject Input; + PackageResourceObject Output; + Execution::Context& ParentContext; + std::unique_ptr<Execution::Context> SubContext; + + // Reset the state that is modified by Get + void Reset() + { + Output = Input.CopyForOutput(); + + SubContext = ParentContext.CreateSubContext(); + SubContext->SetFlags(Execution::ContextFlag::DisableInteractivity); + + if (Input.AcceptAgreements().value_or(false)) + { + SubContext->Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); + SubContext->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + } + } + + void PrepareSubContextInputs() + { + if (Input.Source()) + { + SubContext->Args.AddArg(Execution::Args::Type::Source, Input.Source().value()); + } + } + + // Fills the Output object with the current state + bool Get() + { + PrepareSubContextInputs(); + + *SubContext << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Workflow::DetermineInstalledSource(*SubContext), false, CompositeSearchBehavior::AllPackages); + + if (SubContext->IsTerminated()) + { + ParentContext.Terminate(SubContext->GetTerminationHR()); + return false; + } + + // Do a manual search of the now opened source + Source& source = SubContext->Get<Execution::Data::Source>(); + MatchType matchType = ToMatchType(Input.MatchOption()).value_or(MatchType::CaseInsensitive); + + SearchRequest request; + request.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, Input.Identifier().value())); + + SearchResult result = source.Search(request); + SubContext->Add<Execution::Data::SearchResult>(result); + + if (result.Matches.empty()) + { + Output.Exist(false); + } + else if (result.Matches.size() > 1) + { + AICLI_LOG(Config, Warning, << "Found " << result.Matches.size() << " matches when searching for '" << Input.Identifier().value() << "'"); + Output.Exist(false); + } + else + { + auto& package = result.Matches.front().Package; + SubContext->Add<Execution::Data::Package>(package); + + auto installedPackage = package->GetInstalled(); + + // Fill Output and SubContext + Output.Exist(static_cast<bool>(installedPackage)); + Output.Identifier(package->GetProperty(PackageProperty::Id)); + + if (installedPackage) + { + auto versionKeys = installedPackage->GetVersionKeys(); + AICLI_LOG(CLI, Verbose, << "Package::Get found " << versionKeys.size() << " installed versions"); + + std::shared_ptr<Repository::IPackageVersion> installedVersion; + + // Find the specific version provided if possible + if (Input.Version()) + { + Utility::Version inputVersion{ Input.Version().value() }; + + for (const auto& key : versionKeys) + { + if (inputVersion == Utility::Version{ key.Version }) + { + installedVersion = installedPackage->GetVersion(key); + break; + } + } + } + + if (!installedVersion) + { + installedVersion = installedPackage->GetLatestVersion(); + } + + if (installedVersion) + { + Output.Version(installedVersion->GetProperty(PackageVersionProperty::Version)); + } + + auto data = Repository::GetLatestApplicableVersion(package); + Output.UseLatest(!data.UpdateAvailable); + } + } + + AICLI_LOG(CLI, Verbose, << "Package::Get found:\n" << Json::writeString(Json::StreamWriterBuilder{}, Output.ToJson())); + return true; + } + + void Uninstall() + { + AICLI_LOG(CLI, Verbose, << "Package::Uninstall invoked"); + + if (Input.Version()) + { + SubContext->Args.AddArg(Execution::Args::Type::TargetVersion, Input.Version().value()); + } + else + { + SubContext->Args.AddArg(Execution::Args::Type::AllVersions); + } + + *SubContext << + Workflow::UninstallSinglePackage; + + if (SubContext->IsTerminated()) + { + ParentContext.Terminate(SubContext->GetTerminationHR()); + return; + } + + Output.Exist(false); + Output.Version(std::nullopt); + Output.UseLatest(std::nullopt); + } + + void Install(bool allowDowngrade = false) + { + AICLI_LOG(CLI, Verbose, << "Package::Install invoked"); + + if (Input.Version()) + { + SubContext->Args.AddArg(Execution::Args::Type::Version, Input.Version().value()); + } + + *SubContext << + Workflow::SelectSinglePackageVersionForInstallOrUpgrade(Workflow::OperationType::Install, allowDowngrade) << + Workflow::InstallSinglePackage; + + if (SubContext->IsTerminated()) + { + ParentContext.Terminate(SubContext->GetTerminationHR()); + return; + } + + Output.Exist(true); + + Output.Version(std::nullopt); + if (SubContext->Contains(Execution::Data::PackageVersion)) + { + const auto& packageVersion = SubContext->Get<Execution::Data::PackageVersion>(); + if (packageVersion) + { + Output.Version(packageVersion->GetProperty(Repository::PackageVersionProperty::Version)); + } + } + + Output.UseLatest(std::nullopt); + } + + void Reinstall() + { + AICLI_LOG(CLI, Verbose, << "Package::Reinstall invoked"); + + SubContext->Args.AddArg(Execution::Args::Type::UninstallPrevious); + + Install(true); + } + + // Determines if the current Output values match the Input values state. + bool Test() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + if (Input.ShouldExist()) + { + if (Output.Exist().value()) + { + AICLI_LOG(CLI, Verbose, << "Package::Test needed to inspect these properties: Version(" << TestVersion() << "), Latest(" << TestLatest() << ")"); + return TestVersion() && TestLatest(); + } + else + { + AICLI_LOG(CLI, Verbose, << "Package::Test was false because the package was not installed"); + return false; + } + } + else + { + AICLI_LOG(CLI, Verbose, << "Package::Test desired the package to not exist, and it " << (Output.Exist().value() ? "did" : "did not")); + return !Output.Exist().value(); + } + } + + Json::Value DiffJson() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + Json::Value result{ Json::ValueType::arrayValue }; + + if (Input.ShouldExist() != Output.Exist().value()) + { + result.append(std::string{ StandardExistProperty::Name() }); + } + else + { + if (!TestVersion()) + { + result.append(std::string{ VersionProperty::Name() }); + } + + if (!TestLatest()) + { + result.append(std::string{ UseLatestProperty::Name() }); + } + } + + return result; + } + + bool TestVersion() + { + if (Input.Version()) + { + if (Output.Version()) + { + return Utility::Version{ Input.Version().value() } == Utility::Version{ Output.Version().value() }; + } + else + { + return false; + } + } + else + { + return true; + } + } + + bool TestLatest() + { + if (Input.UseLatest() && Input.UseLatest().value()) + { + if (Output.UseLatest()) + { + return Output.UseLatest().value(); + } + else + { + return false; + } + } + else + { + return true; + } + } + }; + } + + DscPackageResource::DscPackageResource(std::string_view parent) : + DscCommandBase(parent, "package", DscResourceKind::Resource, + DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, + DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff) + { + } + + Resource::LocString DscPackageResource::ShortDescription() const + { + return Resource::String::DscPackageResourceShortDescription; + } + + Resource::LocString DscPackageResource::LongDescription() const + { + return Resource::String::DscPackageResourceLongDescription; + } + + std::string DscPackageResource::ResourceType() const + { + return "Package"; + } + + void DscPackageResource::ResourceFunctionGet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + PackageFunctionData data{ context, json }; + + if (!data.Get()) + { + return; + } + + WriteJsonOutputLine(context, data.Output.ToJson()); + } + } + + void DscPackageResource::ResourceFunctionSet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + PackageFunctionData data{ context, json }; + + if (!data.Get()) + { + return; + } + + // Capture the diff before updating the output + auto diff = data.DiffJson(); + + if (!data.Test()) + { + if (data.Input.ShouldExist()) + { + if (data.Output.Exist().value()) + { + if (!data.TestLatest()) + { + // Install will swap to update flow + AICLI_LOG(CLI, Info, << "Installing package to update to latest"); + data.Install(); + } + else // (!data.TestVersion()) + { + Utility::Version inputVersion{ data.Input.Version().value() }; + Utility::Version outputVersion{ data.Output.Version().value() }; + + if (outputVersion < inputVersion) + { + // Install will swap to update flow + AICLI_LOG(CLI, Info, << "Installing package to update to desired version"); + data.Install(); + } + else + { + AICLI_LOG(CLI, Info, << "Reinstalling package to downgrade to desired version"); + data.Reinstall(); + } + } + } + else + { + AICLI_LOG(CLI, Info, << "Installing package as it was not found"); + data.Install(); + } + } + else + { + AICLI_LOG(CLI, Info, << "Uninstalling package as desired"); + data.Uninstall(); + } + + if (data.SubContext->IsTerminated()) + { + return; + } + } + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, diff); + } + } + + void DscPackageResource::ResourceFunctionTest(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + PackageFunctionData data{ context, json }; + + if (!data.Get()) + { + return; + } + + data.Output.InDesiredState(data.Test()); + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson()); + } + } + + void DscPackageResource::ResourceFunctionExport(Execution::Context& context) const + { + auto json = GetJsonFromInput(context, false); + PackageFunctionData data{ context, json, true }; + + data.PrepareSubContextInputs(); + + if (!data.Input.UseLatest().value_or(true)) + { + data.SubContext->Args.AddArg(Execution::Args::Type::IncludeVersions); + } + + data.SubContext->Args.AddArg(Execution::Args::Type::ConfigurationExportAll); + + *data.SubContext << + Workflow::SearchSourceForPackageExport; + + if (data.SubContext->IsTerminated()) + { + context.Terminate(data.SubContext->GetTerminationHR()); + return; + } + + const auto& packageCollection = data.SubContext->Get<Execution::Data::PackageCollection>(); + + for (const auto& source : packageCollection.Sources) + { + for (const auto& package : source.Packages) + { + PackageResourceObject output; + + output.Identifier(package.Id); + output.Source(source.Details.Name); + + if (!package.VersionAndChannel.GetVersion().IsEmpty()) + { + output.Version(package.VersionAndChannel.GetVersion().ToString()); + } + + // TODO: Exporting scope requires one or more of the following: + // 1. Support for "OrUnknown" scope variants during Set (and workflows) + // 2. Tracking scope intent as we do for some other installer properties + // 3. Checking for the availability of the current scope in the package + + WriteJsonOutputLine(context, output.ToJson()); + } + } + } + + void DscPackageResource::ResourceFunctionSchema(Execution::Context& context) const + { + WriteJsonOutputLine(context, PackageResourceObject::Schema(ResourceType())); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscPackageResource.h b/src/AppInstallerCLICore/Commands/DscPackageResource.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DscCommandBase.h" + +namespace AppInstaller::CLI +{ + // A resource for managing package state. + struct DscPackageResource : public DscCommandBase + { + DscPackageResource(std::string_view parent); + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + std::string ResourceType() const override; + + void ResourceFunctionGet(Execution::Context& context) const override; + void ResourceFunctionSet(Execution::Context& context) const override; + void ResourceFunctionTest(Execution::Context& context) const override; + void ResourceFunctionExport(Execution::Context& context) const override; + void ResourceFunctionSchema(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/DscTestFileResource.cpp b/src/AppInstallerCLICore/Commands/DscTestFileResource.cpp @@ -8,10 +8,10 @@ using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI { - namespace anon + namespace { - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(PathProperty, std::string, Path, "path", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, "The absolute path to a file."); - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ContentProperty, std::string, Content, "content", "The content of the file."); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(PathProperty, std::string, Path, "path", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, "The absolute path to a file."_lis); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ContentProperty, std::string, Content, "content", "The content of the file."_lis); using TestFileObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, PathProperty, ContentProperty>; @@ -127,7 +127,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestFileFunctionData data{ json }; + TestFileFunctionData data{ json }; data.Get(); @@ -139,7 +139,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestFileFunctionData data{ json }; + TestFileFunctionData data{ json }; data.Get(); @@ -186,7 +186,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestFileFunctionData data{ json }; + TestFileFunctionData data{ json }; data.Get(); data.Output.InDesiredState(data.Test()); @@ -200,7 +200,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestFileFunctionData data{ json }; + TestFileFunctionData data{ json }; if (std::filesystem::exists(data.Path)) { @@ -215,7 +215,7 @@ namespace AppInstaller::CLI { if (std::filesystem::is_regular_file(file)) { - anon::TestFileObject output; + TestFileObject output; output.Path(file.path().u8string()); std::ifstream stream{ file.path(), std::ios::binary}; @@ -231,6 +231,6 @@ namespace AppInstaller::CLI void DscTestFileResource::ResourceFunctionSchema(Execution::Context& context) const { - WriteJsonOutputLine(context, anon::TestFileObject::Schema(ResourceType())); + WriteJsonOutputLine(context, TestFileObject::Schema(ResourceType())); } } diff --git a/src/AppInstallerCLICore/Commands/DscTestJsonResource.cpp b/src/AppInstallerCLICore/Commands/DscTestJsonResource.cpp @@ -9,10 +9,10 @@ using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI { - namespace anon + namespace { - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(PropertyProperty, std::string, Property, "property", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, "The JSON property name."); - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ValueProperty, Json::Value, Value, "value", "The value for the JSON property."); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(PropertyProperty, std::string, Property, "property", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, "The JSON property name."_lis); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ValueProperty, Json::Value, Value, "value", "The value for the JSON property."_lis); using TestJsonObject = DscComposableObject<StandardExistProperty, PropertyProperty, ValueProperty>; @@ -119,7 +119,7 @@ namespace AppInstaller::CLI { if (context.Args.Contains(Execution::Args::Type::DscResourceFunctionDelete)) { - std::filesystem::remove_all(anon::TestJsonFunctionData::GetFilePath()); + std::filesystem::remove_all(TestJsonFunctionData::GetFilePath()); return; } @@ -135,7 +135,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestJsonFunctionData data{ json }; + TestJsonFunctionData data{ json }; data.Get(); @@ -147,7 +147,7 @@ namespace AppInstaller::CLI { if (auto json = GetJsonFromInput(context)) { - anon::TestJsonFunctionData data{ json }; + TestJsonFunctionData data{ json }; data.Get(); @@ -181,7 +181,7 @@ namespace AppInstaller::CLI void DscTestJsonResource::ResourceFunctionExport(Execution::Context& context) const { - anon::TestJsonFunctionData data; + TestJsonFunctionData data; if (data.RootValue.isObject()) { @@ -191,7 +191,7 @@ namespace AppInstaller::CLI if (memberValue) { - anon::TestJsonObject output; + TestJsonObject output; output.Property(member); output.Value(*memberValue); @@ -203,6 +203,6 @@ namespace AppInstaller::CLI void DscTestJsonResource::ResourceFunctionSchema(Execution::Context& context) const { - WriteJsonOutputLine(context, anon::TestJsonObject::Schema(ResourceType())); + WriteJsonOutputLine(context, TestJsonObject::Schema(ResourceType())); } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -83,7 +83,7 @@ namespace AppInstaller::CLI Logging::UseGlobalTelemetryLoggerActivityIdOnly(); - Execution::Context context{ std::cout, std::cin }; + Execution::Context context; auto previousThreadGlobals = context.SetForCurrentThread(); // Set up debug string logging during initialization diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -166,6 +166,7 @@ namespace AppInstaller::CLI::Execution OpenLogs, // Opens the default logs directory after executing the command Force, // Forces the execution of the workflow with non security related issues OutputFile, + Correlation, DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -93,6 +93,7 @@ namespace AppInstaller::CLI::Execution // arguments via Execution::Args. struct Context : EnumBasedVariantMap<Data, details::DataMapping, ContextEnumBasedVariantMapActionCallback> { + Context() = default; Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} // Constructor for creating a sub-context. diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -23,6 +23,29 @@ namespace AppInstaller::CLI::Execution const Sequence& ConfigurationUnitEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& AuthenticationEmphasis = TextFormat::Foreground::BrightYellow; + namespace + { + DWORD GetStdHandleType(DWORD stdHandle) + { + DWORD result = FILE_TYPE_UNKNOWN; + + HANDLE handle = GetStdHandle(stdHandle); + if (handle != INVALID_HANDLE_VALUE && handle != NULL) + { + result = GetFileType(handle); + } + + return result; + } + } + + Reporter::Reporter() : + Reporter(std::cout, std::cin) + { + m_outStreamFileType = GetStdHandleType(STD_OUTPUT_HANDLE); + m_inStreamFileType = GetStdHandleType(STD_INPUT_HANDLE); + } + Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : Reporter(std::make_shared<BaseStream>(outStream, true, ConsoleModeRestore::Instance().IsVTEnabled()), inStream) { @@ -48,6 +71,11 @@ namespace AppInstaller::CLI::Execution Reporter::Reporter(const Reporter& other, clone_t) : Reporter(other.m_out, other.m_in) { + m_outStreamFileType = other.m_outStreamFileType; + m_inStreamFileType = other.m_inStreamFileType; + + SetChannel(other.m_channel); + if (other.m_style.has_value()) { SetStyle(*other.m_style); @@ -136,6 +164,12 @@ namespace AppInstaller::CLI::Execution return m_in; } + bool Reporter::InputStreamIsInteractive() const + { + AICLI_LOG(CLI, Verbose, << "Reporter::m_inStreamFileType is " << m_inStreamFileType); + return m_inStreamFileType == FILE_TYPE_CHAR; + } + bool Reporter::PromptForBoolResponse(Resource::LocString message, Level level, bool resultIfDisabled) { auto out = GetOutputStream(level); diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -60,6 +60,7 @@ namespace AppInstaller::CLI::Execution All = Verbose | Info | Warning | Error, }; + Reporter(); Reporter(std::ostream& outStream, std::istream& inStream); Reporter(const Reporter&) = delete; Reporter& operator=(const Reporter&) = delete; @@ -109,6 +110,9 @@ namespace AppInstaller::CLI::Execution // Get the raw input stream. std::istream& RawInputStream(); + // Check if the input stream is interactive or not. + bool InputStreamIsInteractive() const; + // Prompts the user, return true if they consented. bool PromptForBoolResponse(Resource::LocString message, Level level = Level::Info, bool resultIfDisabled = false); @@ -204,6 +208,8 @@ namespace AppInstaller::CLI::Execution wil::srwlock m_progressCallbackLock; std::atomic<ProgressCallback*> m_progressCallback; std::atomic<IProgressSink*> m_progressSink; + DWORD m_outStreamFileType = FILE_TYPE_UNKNOWN; + DWORD m_inStreamFileType = FILE_TYPE_UNKNOWN; // Enable all levels by default Level m_enabledLevels = Level::All; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -184,6 +184,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigureValidateCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureValidateCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConvertInstallFlowToUpgrade); + WINGET_DEFINE_RESOURCE_STRINGID(CorrelationArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(CustomSwitchesArgumentDescription); @@ -212,6 +213,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Downloading); WINGET_DEFINE_RESOURCE_STRINGID(DscCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscPackageResourceShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscPackageResourceLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionGet); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionSet); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionWhatIf); @@ -223,6 +226,16 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionAdapter); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionSchema); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionManifest); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionExist); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionInDesiredState); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageId); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageSource); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageVersion); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageScope); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageMatchOption); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageUseLatest); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageInstallMode); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageAcceptAgreements); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(EnableWindowsFeaturesSuccess); WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature); diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "DependenciesFlow.h" -#include "ManifestComparator.h" +#include <winget/ManifestComparator.h> #include "InstallFlow.h" #include "winget\DependenciesGraph.h" #include "DependencyNodeProcessor.h" diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "DependencyNodeProcessor.h" -#include "ManifestComparator.h" +#include "WorkflowBase.h" #include <winget/PinningData.h> #include <winget/PackageVersionSelection.h> @@ -96,7 +96,7 @@ namespace AppInstaller::CLI::Workflow installationMetadata = m_nodePackageInstalledVersion->GetMetadata(); } - ManifestComparator manifestComparator(m_context, installationMetadata); + ManifestComparator manifestComparator(GetManifestComparatorOptions(m_context, installationMetadata)); auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(m_nodeManifest); if (!installer.has_value()) @@ -112,4 +112,4 @@ namespace AppInstaller::CLI::Workflow m_dependenciesList = m_installer.Dependencies; return DependencyNodeProcessorResult::Success; } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -1,962 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "ManifestComparator.h" -#include "WorkflowBase.h" -#include <AppInstallerLogging.h> -#include <winget/UserSettings.h> -#include <winget/Runtime.h> -#include <winget/Locale.h> - -using namespace AppInstaller::CLI; -using namespace AppInstaller::Manifest; - -namespace AppInstaller::Manifest -{ - std::ostream& operator<<(std::ostream& out, const AppInstaller::Manifest::ManifestInstaller& installer) - { - return out << '[' << - AppInstaller::Utility::ToString(installer.Arch) << ',' << - AppInstaller::Manifest::InstallerTypeToString(installer.EffectiveInstallerType()) << ',' << - AppInstaller::Manifest::ScopeToString(installer.Scope) << ',' << - installer.Locale << ']'; - } -} - -namespace AppInstaller::CLI::Workflow -{ - namespace - { - struct PortableInstallFilter : public details::FilterField - { - PortableInstallFilter() : details::FilterField("Portable Install") {} - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - // Unvirtualized resources restricted capability is only supported for >= 10.0.18362 - // TODO: Add support for OS versions that don't support virtualization. - if (installer.EffectiveInstallerType() == InstallerTypeEnum::Portable && !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version("10.0.18362"))) - { - return InapplicabilityFlags::OSVersion; - } - - return InapplicabilityFlags::None; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller&) override - { - std::string result = "Current OS is lower than supported MinOSVersion (10.0.18362) for Portable install"; - return result; - } - }; - - struct OSVersionFilter : public details::FilterField - { - OSVersionFilter() : details::FilterField("OS Version") {} - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - if (installer.MinOSVersion.empty() || Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion))) - { - return InapplicabilityFlags::None; - } - - return InapplicabilityFlags::OSVersion; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Current OS is lower than MinOSVersion "; - result += installer.MinOSVersion; - return result; - } - }; - - struct MachineArchitectureComparator : public details::ComparisonField - { - MachineArchitectureComparator() : details::ComparisonField("Machine Architecture") {} - - MachineArchitectureComparator(std::vector<Utility::Architecture> allowedArchitectures) : - details::ComparisonField("Machine Architecture"), m_allowedArchitectures(std::move(allowedArchitectures)) - { - AICLI_LOG(CLI, Verbose, << "Architecture Comparator created with allowed architectures: " << Utility::ConvertContainerToString(m_allowedArchitectures, Utility::ToString)); - } - - static std::unique_ptr<MachineArchitectureComparator> Create(const Execution::Context& context, const Repository::IPackageVersion::Metadata& metadata) - { - std::vector<Utility::Architecture> allowedArchitectures; - bool skipApplicabilityCheck = false; - - if (context.Contains(Execution::Data::AllowedArchitectures)) - { - // Com caller can directly set allowed architectures - allowedArchitectures = context.Get<Execution::Data::AllowedArchitectures>(); - } - else if (context.Args.Contains(Execution::Args::Type::InstallArchitecture)) - { - // Arguments provided in command line - allowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallArchitecture))); - } - else if (context.Args.Contains(Execution::Args::Type::InstallerArchitecture)) - { - // Arguments provided in command line. Also skips applicability check. - allowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallerArchitecture))); - skipApplicabilityCheck = true; - } - else - { - auto userIntentItr = metadata.find(Repository::PackageVersionMetadata::UserIntentArchitecture); - auto installedItr = metadata.find(Repository::PackageVersionMetadata::InstalledArchitecture); - if (userIntentItr != metadata.end()) - { - // For upgrade, user intent from previous install is considered requirement - allowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(userIntentItr->second)); - } - else - { - if (installedItr != metadata.end()) - { - // For upgrade, previous installed architecture should be considered first preference and is always allowed. - // Then check settings requirements and preferences. - allowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(installedItr->second)); - } - - std::vector<Utility::Architecture> requiredArchitectures = Settings::User().Get<Settings::Setting::InstallArchitectureRequirement>(); - std::vector<Utility::Architecture> optionalArchitectures = Settings::User().Get<Settings::Setting::InstallArchitecturePreference>(); - - if (!requiredArchitectures.empty()) - { - // Required architecture list from settings if applicable - allowedArchitectures.insert(allowedArchitectures.end(), requiredArchitectures.begin(), requiredArchitectures.end()); - } - else - { - // Preferred architecture list from settings if applicable, add Unknown to indicate allowing remaining applicable - if (!optionalArchitectures.empty()) - { - allowedArchitectures.insert(allowedArchitectures.end(), optionalArchitectures.begin(), optionalArchitectures.end()); - } - - allowedArchitectures.emplace_back(Utility::Architecture::Unknown); - } - } - } - - if (!allowedArchitectures.empty()) - { - // If the incoming data contains elements, we will use them to construct a final allowed list. - // The algorithm is to take elements until we find Unknown, which indicates that any architecture is - // acceptable at this point. The system supported set of architectures will then be placed at the end. - std::vector<Utility::Architecture> result; - bool addRemainingApplicableArchitectures = false; - - for (Utility::Architecture architecture : allowedArchitectures) - { - if (architecture == Utility::Architecture::Unknown) - { - addRemainingApplicableArchitectures = true; - break; - } - - // If the architecture is applicable and not already in our result set... - if ((skipApplicabilityCheck || Utility::IsApplicableArchitecture(architecture) != Utility::InapplicableArchitecture) && - Utility::IsApplicableArchitecture(architecture, result) == Utility::InapplicableArchitecture) - { - result.push_back(architecture); - } - } - - if (addRemainingApplicableArchitectures) - { - for (Utility::Architecture architecture : Utility::GetApplicableArchitectures()) - { - if (Utility::IsApplicableArchitecture(architecture, result) == Utility::InapplicableArchitecture) - { - result.push_back(architecture); - } - } - } - - return std::make_unique<MachineArchitectureComparator>(std::move(result)); - } - else - { - return std::make_unique<MachineArchitectureComparator>(); - } - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - if (CheckAllowedArchitecture(installer.Arch) == Utility::InapplicableArchitecture || - IsSystemArchitectureUnsupportedByInstaller(installer)) - { - return InapplicabilityFlags::MachineArchitecture; - } - - return InapplicabilityFlags::None; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result; - if (Utility::IsApplicableArchitecture(installer.Arch) == Utility::InapplicableArchitecture) - { - result = "Machine is not compatible with "; - result += Utility::ToString(installer.Arch); - } - else if (IsSystemArchitectureUnsupportedByInstaller(installer)) - { - result = "System architecture is unsupported by installer"; - } - else - { - result = "Architecture was excluded by caller : "; - result += Utility::ToString(installer.Arch); - } - - return result; - } - - details::ComparisonResult IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override - { - auto arch1 = CheckAllowedArchitecture(first.Arch); - auto arch2 = CheckAllowedArchitecture(second.Arch); - - if (arch1 > arch2) - { - // A match with the primary architecture is strong - return (first.Arch == GetStrongArchitectureMatch() ? details::ComparisonResult::StrongPositive : details::ComparisonResult::WeakPositive); - } - - return details::ComparisonResult::Negative; - } - - private: - int CheckAllowedArchitecture(Utility::Architecture architecture) - { - if (m_allowedArchitectures.empty()) - { - return Utility::IsApplicableArchitecture(architecture); - } - else - { - return Utility::IsApplicableArchitecture(architecture, m_allowedArchitectures); - } - } - - bool IsSystemArchitectureUnsupportedByInstaller(const ManifestInstaller& installer) - { - auto unsupportedItr = std::find( - installer.UnsupportedOSArchitectures.begin(), - installer.UnsupportedOSArchitectures.end(), - Utility::GetSystemArchitecture()); - return unsupportedItr != installer.UnsupportedOSArchitectures.end(); - } - - Utility::Architecture GetStrongArchitectureMatch() - { - // If we have a preferential order, treat the first entry as strong. - // Otherwise, treat the system architecture as strong (which is always first in the default order). - return m_allowedArchitectures.empty() ? Utility::GetSystemArchitecture() : m_allowedArchitectures.front(); - } - - std::vector<Utility::Architecture> m_allowedArchitectures; - }; - - struct InstallerTypeComparator : public details::ComparisonField - { - 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 - << " , 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 (!preference.empty() || !requirement.empty()) - { - return std::make_unique<InstallerTypeComparator>(preference, requirement); - } - else - { - return {}; - } - } - - 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()) - { - // The installer is applicable if the effective or base installer type matches. - if (ContainsInstallerType(m_requirement, installer.EffectiveInstallerType()) || - ContainsInstallerType(m_requirement, installer.BaseInstallerType)) - { - return InapplicabilityFlags::None; - } - - return InapplicabilityFlags::InstallerType; - } - else - { - return InapplicabilityFlags::None; - } - } - - details::ComparisonResult IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override - { - if (m_preference.empty()) - { - return details::ComparisonResult::Negative; - } - - 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 details::ComparisonResult::Negative; - } - else if (isFirstInstallerTypePreferred != isSecondInstallerTypePreferred) - { - // Treating this as a weak positive because one can use requirements to guarantee the installer type if necessary. - return (isFirstInstallerTypePreferred ? details::ComparisonResult::WeakPositive : details::ComparisonResult::Negative); - } - } - - return details::ComparisonResult::Negative; - } - - 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 InstalledTypeFilter : public details::FilterField - { - InstalledTypeFilter(Manifest::InstallerTypeEnum installedType) : - details::FilterField("Installed Type"), m_installedType(installedType) {} - - static std::unique_ptr<InstalledTypeFilter> Create(const Repository::IPackageVersion::Metadata& installationMetadata) - { - auto installerTypeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledType); - if (installerTypeItr != installationMetadata.end()) - { - Manifest::InstallerTypeEnum installedType = Manifest::ConvertToInstallerTypeEnum(installerTypeItr->second); - if (installedType != Manifest::InstallerTypeEnum::Unknown) - { - return std::make_unique<InstalledTypeFilter>(installedType); - } - } - - return {}; - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - return IsInstallerCompatibleWith(installer, m_installedType) ? InapplicabilityFlags::None : InapplicabilityFlags::InstalledType; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Installed package type '" + std::string{ Manifest::InstallerTypeToString(m_installedType) } + - "' is not compatible with installer type " + std::string{ Manifest::InstallerTypeToString(installer.EffectiveInstallerType()) }; - - std::string arpInstallerTypes; - for (const auto& entry : installer.AppsAndFeaturesEntries) - { - arpInstallerTypes += " " + std::string{ Manifest::InstallerTypeToString(entry.InstallerType) }; - } - - if (!arpInstallerTypes.empty()) - { - result += ", or with accepted type(s)" + arpInstallerTypes; - } - - return result; - } - - private: - // The installer is compatible if it's type or any of its ARP entries' type matches the installed type - static bool IsInstallerCompatibleWith(const Manifest::ManifestInstaller& installer, Manifest::InstallerTypeEnum type) - { - if (Manifest::IsInstallerTypeCompatible(installer.EffectiveInstallerType(), type)) - { - return true; - } - - auto itr = std::find_if( - installer.AppsAndFeaturesEntries.begin(), - installer.AppsAndFeaturesEntries.end(), - [=](AppsAndFeaturesEntry arpEntry) { return Manifest::IsInstallerTypeCompatible(arpEntry.InstallerType, type); }); - if (itr != installer.AppsAndFeaturesEntries.end()) - { - return true; - } - - return false; - } - - Manifest::InstallerTypeEnum m_installedType; - }; - - struct InstalledScopeFilter : public details::FilterField - { - InstalledScopeFilter(Manifest::ScopeEnum requirement) : - details::FilterField("Installed Scope"), m_requirement(requirement) {} - - static std::unique_ptr<InstalledScopeFilter> Create(const Repository::IPackageVersion::Metadata& installationMetadata) - { - // Check for an existing install and require a matching scope. - auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); - if (installerScopeItr != installationMetadata.end()) - { - Manifest::ScopeEnum installedScope = Manifest::ConvertToScopeEnum(installerScopeItr->second); - if (installedScope != Manifest::ScopeEnum::Unknown) - { - return std::make_unique<InstalledScopeFilter>(installedScope); - } - } - - return {}; - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - // We have to assume the unknown scope will match our required scope, or the entire catalog would stop working for upgrade. - if (installer.Scope == Manifest::ScopeEnum::Unknown || installer.Scope == m_requirement || DoesInstallerTypeIgnoreScopeFromManifest(installer.EffectiveInstallerType())) - { - return InapplicabilityFlags::None; - } - - return InapplicabilityFlags::InstalledScope; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Installer scope does not match currently installed scope: "; - result += Manifest::ScopeToString(installer.Scope); - result += " != "; - result += Manifest::ScopeToString(m_requirement); - return result; - } - - private: - Manifest::ScopeEnum m_requirement; - }; - - struct ScopeComparator : public details::ComparisonField - { - 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<ScopeComparator> Create(const Execution::Context& context) - { - // Preference will always come from settings - Manifest::ScopeEnum preference = Settings::User().Get<Settings::Setting::InstallScopePreference>(); - - // 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)); - } - else - { - requirement = Settings::User().Get<Settings::Setting::InstallScopeRequirement>(); - } - - bool allowUnknownInAdditionToRequired = false; - if (context.Contains(Execution::Data::AllowUnknownScope)) - { - allowUnknownInAdditionToRequired = context.Get<Execution::Data::AllowUnknownScope>(); - - // 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<ScopeComparator>(preference, requirement, allowUnknownInAdditionToRequired); - } - else - { - return {}; - } - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - // 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) || - DoesInstallerTypeIgnoreScopeFromManifest(installer.EffectiveInstallerType())) - { - return InapplicabilityFlags::None; - } - - return InapplicabilityFlags::Scope; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Installer scope does not match required scope: "; - result += Manifest::ScopeToString(installer.Scope); - result += " != "; - result += Manifest::ScopeToString(m_requirement); - return result; - } - - details::ComparisonResult IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override - { - if (m_preference != Manifest::ScopeEnum::Unknown && first.Scope == m_preference && second.Scope != m_preference) - { - // When the second input is unknown, this is a weak result. If it is not (and therefore the opposite of the preference), this is strong. - return (second.Scope == Manifest::ScopeEnum::Unknown ? details::ComparisonResult::WeakPositive : details::ComparisonResult::StrongPositive); - } - - return details::ComparisonResult::Negative; - } - - private: - Manifest::ScopeEnum m_preference; - Manifest::ScopeEnum m_requirement; - bool m_allowUnknownInAdditionToRequired; - }; - - struct LocaleComparator : public details::ComparisonField - { - LocaleComparator(std::vector<std::string> preference, std::vector<std::string> requirement, bool isInstalledLocale) : - details::ComparisonField("Locale"), m_preference(std::move(preference)), m_requirement(std::move(requirement)), m_isInstalledLocale(isInstalledLocale) - { - m_requirementAsString = Utility::ConvertContainerToString(m_requirement); - m_preferenceAsString = Utility::ConvertContainerToString(m_preference); - AICLI_LOG(CLI, Verbose, - << "Locale Comparator created with Required Locales: " << m_requirementAsString - << " , Preferred Locales: " << m_preferenceAsString - << " , IsInstalledLocale: " << m_isInstalledLocale); - } - - static std::unique_ptr<LocaleComparator> Create(const Execution::Args& args, const Repository::IPackageVersion::Metadata& metadata) - { - std::vector<std::string> preference; - std::vector<std::string> requirement; - // This is for installed locale case, where the locale is a preference but requires at least compatible match. - bool isInstalledLocale = false; - - auto userIntentItr = metadata.find(Repository::PackageVersionMetadata::UserIntentLocale); - auto installedItr = metadata.find(Repository::PackageVersionMetadata::InstalledLocale); - // Requirement may come from args, previous user intent or settings; args overrides previous user intent then settings. - if (args.Contains(Execution::Args::Type::Locale)) - { - requirement.emplace_back(args.GetArg(Execution::Args::Type::Locale)); - } - else if (userIntentItr != metadata.end()) - { - requirement.emplace_back(userIntentItr->second); - isInstalledLocale = true; - } - else - { - if (installedItr == metadata.end()) - { - // If it's an upgrade of previous package, no need to set requirements from settings - // as previous installed locale will be used later. - requirement = Settings::User().Get<Settings::Setting::InstallLocaleRequirement>(); - } - } - - // Preference will come from previous installed locale, winget settings or Preferred Languages settings. - // Previous installed locale goes first, then winget settings, then Preferred Languages settings. - // Previous installed locale also requires at least compatible locale match. - if (installedItr != metadata.end()) - { - preference.emplace_back(installedItr->second); - isInstalledLocale = true; - } - else - { - preference = Settings::User().Get<Settings::Setting::InstallLocalePreference>(); - if (preference.empty()) - { - preference = Locale::GetUserPreferredLanguages(); - } - } - - if (!preference.empty() || !requirement.empty()) - { - return std::make_unique<LocaleComparator>(preference, requirement, isInstalledLocale); - } - else - { - return {}; - } - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - InapplicabilityFlags inapplicableFlag = m_isInstalledLocale ? InapplicabilityFlags::InstalledLocale : InapplicabilityFlags::Locale; - - if (!m_requirement.empty()) - { - // Check if requirement is satisfied - for (auto const& requiredLocale : m_requirement) - { - if (Locale::GetDistanceOfLanguage(requiredLocale, installer.Locale) >= Locale::MinimumDistanceScoreAsPerfectMatch) - { - return InapplicabilityFlags::None; - } - } - - return inapplicableFlag; - } - else if (m_isInstalledLocale && !m_preference.empty()) - { - // For installed locale preference, check at least compatible match for preference - for (auto const& preferredLocale : m_preference) - { - // We have to assume an unknown installer locale will match our installed locale, or the entire catalog would stop working for upgrade. - if (installer.Locale.empty() || - Locale::GetDistanceOfLanguage(preferredLocale, installer.Locale) >= Locale::MinimumDistanceScoreAsCompatibleMatch) - { - return InapplicabilityFlags::None; - } - } - - return inapplicableFlag; - } - else - { - return InapplicabilityFlags::None; - } - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Installer locale does not match required locale: "; - result += installer.Locale; - result += "Required locales: "; - result += m_requirementAsString; - result += " Or does not satisfy compatible match for Preferred Locales: "; - result += m_preferenceAsString; - return result; - } - - details::ComparisonResult IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override - { - if (m_preference.empty()) - { - return details::ComparisonResult::Negative; - } - - for (auto const& preferredLocale : m_preference) - { - double firstScore = first.Locale.empty() ? Locale::UnknownLanguageDistanceScore : Locale::GetDistanceOfLanguage(preferredLocale, first.Locale); - double secondScore = second.Locale.empty() ? Locale::UnknownLanguageDistanceScore : Locale::GetDistanceOfLanguage(preferredLocale, second.Locale); - - if (firstScore >= Locale::MinimumDistanceScoreAsCompatibleMatch || secondScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) - { - // This could probably be enriched to always check all locales and determine strong/weak based off of the MinimumDistanceScoreAsCompatibleMatch. - return (firstScore > secondScore ? details::ComparisonResult::StrongPositive : details::ComparisonResult::Negative); - } - } - - // At this point, the installer locale matches no preference. - // if first is unknown and second is no match for sure, we might prefer unknown one. - return (first.Locale.empty() && !second.Locale.empty() ? details::ComparisonResult::WeakPositive : details::ComparisonResult::Negative); - } - - private: - std::vector<std::string> m_preference; - std::vector<std::string> m_requirement; - std::string m_requirementAsString; - std::string m_preferenceAsString; - bool m_isInstalledLocale = false; - }; - - struct MarketFilter : public details::FilterField - { - MarketFilter(Manifest::string_t market) : details::FilterField("Market"), m_market(market) - { - AICLI_LOG(CLI, Verbose, << "Market Filter created with market: " << m_market); - } - - static std::unique_ptr<MarketFilter> Create() - { - return std::make_unique<MarketFilter>(Runtime::GetOSRegion()); - } - - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override - { - // If both allowed and excluded lists are provided, we only need to check the allowed markets. - if (!installer.Markets.AllowedMarkets.empty()) - { - // Inapplicable if NOT found - if (!IsMarketInList(installer.Markets.AllowedMarkets)) - { - return InapplicabilityFlags::Market; - } - } - else if (!installer.Markets.ExcludedMarkets.empty()) - { - // Inapplicable if found - if (IsMarketInList(installer.Markets.ExcludedMarkets)) - { - return InapplicabilityFlags::Market; - } - } - - return InapplicabilityFlags::None; - } - - std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override - { - std::string result = "Current market '" + m_market + "' does not match installer markets." + - " Allowed markets: " + Utility::ConvertContainerToString(installer.Markets.AllowedMarkets) + - " Excluded markets: " + Utility::ConvertContainerToString(installer.Markets.ExcludedMarkets); - return result; - } - - private: - bool IsMarketInList(const std::vector<Manifest::string_t> markets) - { - return markets.end() != std::find_if( - markets.begin(), - markets.end(), - [&](const auto& m) { return Utility::CaseInsensitiveEquals(m, m_market); }); - } - - Manifest::string_t m_market; - }; - } - - ManifestComparator::ManifestComparator(const Execution::Context& context, const Repository::IPackageVersion::Metadata& installationMetadata) - { - // 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 - AddFilter(InstalledScopeFilter::Create(installationMetadata)); - // Filters based on the market region of the system - AddFilter(MarketFilter::Create()); - // Filters based on the installer type compatability, including with AppsAndFeaturesEntry declarations - AddFilter(InstalledTypeFilter::Create(installationMetadata)); - - // Filter order is not important, but comparison order determines priority. - // Note that all comparators are also filters and their comparison function will only be called on - // installers that both match the required criteria. - // - // The comparators are ordered by the `IsFirstBetter` method, which uses the following algorithm: - // - Each comparison between two installers can return one of { Strong, Weak, Negative } - // - Installers are compared in both directions, going through the list of comparators as defined here - // - The first Strong result in either direction is given priority - // - If no Strong results, the first Weak result is used - // - If all Negative results, then the two installers are equal in priority (meaning the first one in the list is kept as "better") - // - // TODO: There are improvements to be made here around ordering, especially in the context of implicit vs explicit vs command line preferences. - - // Filters based on exact matches for requirements or compatible matches for preferences - // Only applies when preference exists: - // Strong if first is compatible and better match than second - // Weak if first is unknown and second is not - AddComparator(LocaleComparator::Create(context.Args, installationMetadata)); - // Filters only if a requirement is present and it cannot be satisfied by the installer (including installer types that we can control scope in code) - // Only applies when preference exists: - // Strong if first matches preference and second does not and is not Unknown - // Weak if first matches preference and second is Unknown - AddComparator(ScopeComparator::Create(context)); - // Filters architectures out that are not supported or are not in the preferences/requirements/inputs. - // Strong if first equals the earliest architecture in the allowed list and second does not [default means the system architecture] - // Weak if first is better match for system architecture than second - AddComparator(MachineArchitectureComparator::Create(context, installationMetadata)); - // Filters installer types out that are not in preferences or requirements. - // Only applies when preference exists: - // Weak if first is in preference list and second is not - AddComparator(InstallerTypeComparator::Create(context.Args)); - } - - InstallerAndInapplicabilities ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) - { - AICLI_LOG(CLI, Verbose, << "Starting installer selection."); - - const Manifest::ManifestInstaller* result = nullptr; - std::vector<InapplicabilityFlags> inapplicabilitiesInstallers; - - for (const auto& installer : manifest.Installers) - { - auto inapplicabilityInstaller = IsApplicable(installer); - if (inapplicabilityInstaller == InapplicabilityFlags::None) - { - if (!result || IsFirstBetter(installer, *result)) - { - AICLI_LOG(CLI, Verbose, << "Installer " << installer << " is current best choice"); - result = &installer; - } - } - else - { - inapplicabilitiesInstallers.push_back(inapplicabilityInstaller); - } - } - - if (!result) - { - return { {}, std::move(inapplicabilitiesInstallers) }; - } - - return { *result, std::move(inapplicabilitiesInstallers) }; - } - - InapplicabilityFlags ManifestComparator::IsApplicable(const Manifest::ManifestInstaller& installer) - { - InapplicabilityFlags inapplicabilityResult = InapplicabilityFlags::None; - - for (const auto& filter : m_filters) - { - auto inapplicability = filter->IsApplicable(installer); - if (inapplicability != InapplicabilityFlags::None) - { - AICLI_LOG(CLI, Verbose, << "Installer " << installer << " not applicable: " << filter->ExplainInapplicable(installer)); - WI_SetAllFlags(inapplicabilityResult, inapplicability); - } - } - - return inapplicabilityResult; - } - - bool ManifestComparator::IsFirstBetter( - const Manifest::ManifestInstaller& first, - const Manifest::ManifestInstaller& second) - { - // The priority will still be used as a tie-break between weak results. - std::optional<std::string_view> firstWeakComparator; - bool firstWeakComparatorResult = false; - - for (auto comparator : m_comparators) - { - details::ComparisonResult forwardCompare = comparator->IsFirstBetter(first, second); - details::ComparisonResult reverseCompare = comparator->IsFirstBetter(second, first); - - // Should not happen, but if it does it points at a serious bug that should be fixed. - if (forwardCompare != details::ComparisonResult::Negative && reverseCompare != details::ComparisonResult::Negative) - { - AICLI_LOG(CLI, Error, << "Installer " << first << " and " << second << " are both better than each other?"); - THROW_HR(E_UNEXPECTED); - } - - if (forwardCompare == details::ComparisonResult::StrongPositive) - { - AICLI_LOG(CLI, Verbose, << "Installer " << first << " is better [strong] than " << second << " due to: " << comparator->Name()); - return true; - } - - if (reverseCompare == details::ComparisonResult::StrongPositive) - { - // Second is better by this comparator, don't allow a lower priority one to override that. - AICLI_LOG(CLI, Verbose, << "Installer " << second << " is better [strong] than " << first << " due to: " << comparator->Name()); - return false; - } - - // Save the first weak result that we get - if (!firstWeakComparator) - { - if (forwardCompare == details::ComparisonResult::WeakPositive) - { - firstWeakComparator = comparator->Name(); - firstWeakComparatorResult = true; - } - else if (reverseCompare == details::ComparisonResult::WeakPositive) - { - firstWeakComparator = comparator->Name(); - firstWeakComparatorResult = false; - } - } - } - - // If we found a weak result (and no strong result because we made it here), return it. - if (firstWeakComparator) - { - if (firstWeakComparatorResult) - { - AICLI_LOG(CLI, Verbose, << "Installer " << first << " is better [weak] than " << second << " due to: " << *firstWeakComparator); - } - else - { - AICLI_LOG(CLI, Verbose, << "Installer " << second << " is better [weak] than " << first << " due to: " << *firstWeakComparator); - } - - return firstWeakComparatorResult; - } - - // Equal, and thus not better - AICLI_LOG(CLI, Verbose, << "Installer " << first << " and " << second << " are equivalent in priority"); - return false; - } - - void ManifestComparator::AddFilter(std::unique_ptr<details::FilterField>&& filter) - { - if (filter) - { - m_filters.emplace_back(std::move(filter)); - } - } - - void ManifestComparator::AddComparator(std::unique_ptr<details::ComparisonField>&& comparator) - { - if (comparator) - { - m_comparators.push_back(comparator.get()); - m_filters.emplace_back(std::move(comparator)); - } - } -} diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "ExecutionContext.h" -#include "ExecutionArgs.h" -#include <winget/Manifest.h> -#include <winget/RepositorySearch.h> - -#include <memory> -#include <string> -#include <string_view> -#include <vector> - - -namespace AppInstaller::CLI::Workflow -{ - // Flags to indicate why an installer was not applicable - enum class InapplicabilityFlags : int - { - None = 0x0, - OSVersion = 0x1, - InstalledScope = 0x2, - InstalledType = 0x4, - InstalledLocale = 0x8, - Locale = 0x10, - Scope = 0x20, - MachineArchitecture = 0x40, - Market = 0x80, - InstallerType = 0x100, - }; - - DEFINE_ENUM_FLAG_OPERATORS(InapplicabilityFlags); - - namespace details - { - // An interface for defining new filters based on user inputs. - struct FilterField - { - FilterField(std::string_view name) : m_name(name) {} - - virtual ~FilterField() = default; - - std::string_view Name() const { return m_name; } - - // Determines if the installer is applicable based on this field alone. - virtual InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) = 0; - - // Explains why the filter regarded this installer as inapplicable. - // Will only be called when IsApplicable returns false. - virtual std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) = 0; - - private: - std::string_view m_name; - }; - - // The result of ComparisonField::IsFirstBetter - enum class ComparisonResult - { - // The first input is not better than the second input. - Negative, - // The first input is somewhat better than the second input. - // If another comparison has a strong positive result, it will override a weak result. - WeakPositive, - // The first input is definitely better than the second input. - StrongPositive, - }; - - // An interface for defining new comparisons based on user inputs. - struct ComparisonField : public FilterField - { - using FilterField::FilterField; - - virtual ~ComparisonField() = default; - - // Determines if the first installer is a better choice based on this field alone. - virtual ComparisonResult IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) = 0; - }; - } - - struct InstallerAndInapplicabilities - { - std::optional<Manifest::ManifestInstaller> installer; - std::vector<InapplicabilityFlags> inapplicabilitiesInstaller; - }; - - // Class in charge of comparing manifest entries - struct ManifestComparator - { - ManifestComparator(const Execution::Context& context, const Repository::IPackageVersion::Metadata& installationMetadata); - - // Gets the best installer from the manifest, if at least one is applicable. - InstallerAndInapplicabilities GetPreferredInstaller(const Manifest::Manifest& manifest); - - // Determines if an installer is applicable. - InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer); - - // Determines if the first installer is a better choice. - bool IsFirstBetter( - const Manifest::ManifestInstaller& first, - const Manifest::ManifestInstaller& second); - - private: - void AddFilter(std::unique_ptr<details::FilterField>&& filter); - void AddComparator(std::unique_ptr<details::ComparisonField>&& comparator); - - std::vector<std::unique_ptr<details::FilterField>> m_filters; - // Non-owning pointers to values in m_filters. - std::vector<details::ComparisonField*> m_comparators; - }; - -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -13,7 +13,7 @@ #include "AppInstallerMsixInfo.h" #include "AppInstallerSynchronization.h" #include "MSStoreInstallerHandler.h" -#include "ManifestComparator.h" +#include <winget/ManifestComparator.h> #include <winget/PackageVersionSelection.h> using namespace AppInstaller::Manifest; diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "ShowFlow.h" -#include "ManifestComparator.h" +#include <winget/ManifestComparator.h> #include "TableOutput.h" using namespace AppInstaller::Repository; diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -5,7 +5,7 @@ #include "DependenciesFlow.h" #include "InstallFlow.h" #include "UpdateFlow.h" -#include "ManifestComparator.h" +#include <winget/ManifestComparator.h> #include <winget/PinningData.h> #include <winget/PackageVersionSelection.h> @@ -51,7 +51,7 @@ namespace AppInstaller::CLI::Workflow installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); } - ManifestComparator manifestComparator(context, isUpgrade ? installedPackage->GetMetadata() : IPackageVersion::Metadata{}); + Manifest::ManifestComparator manifestComparator(GetManifestComparatorOptions(context, isUpgrade ? installedPackage->GetMetadata() : IPackageVersion::Metadata{})); bool versionFound = false; bool installedTypeInapplicable = false; bool packagePinned = false; @@ -116,7 +116,7 @@ namespace AppInstaller::CLI::Workflow if (!installer.has_value()) { // If there is at least one installer whose only reason is InstalledType. - auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), InapplicabilityFlags::InstalledType); + auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), Manifest::InapplicabilityFlags::InstalledType); if (onlyInstalledType != inapplicabilities.end()) { installedTypeInapplicable = true; @@ -330,7 +330,7 @@ namespace AppInstaller::CLI::Workflow // If version specified, use the version and verify applicability context << GetManifestFromPackage(/* considerPins */ true); - if (m_operationType == OperationType::Upgrade) + if (m_operationType == OperationType::Upgrade && !m_allowDowngrade) { context << EnsureUpdateVersionApplicable; } diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.h b/src/AppInstallerCLICore/Workflows/UpdateFlow.h @@ -39,13 +39,14 @@ namespace AppInstaller::CLI::Workflow // Outputs: None struct SelectSinglePackageVersionForInstallOrUpgrade : public WorkflowTask { - SelectSinglePackageVersionForInstallOrUpgrade(OperationType operation) : - WorkflowTask("SelectSinglePackageVersionForInstallOrUpgrade"), m_operationType(operation) {} + SelectSinglePackageVersionForInstallOrUpgrade(OperationType operation, bool allowDowngrade = false) : + WorkflowTask("SelectSinglePackageVersionForInstallOrUpgrade"), m_operationType(operation), m_allowDowngrade(allowDowngrade) {} void operator()(Execution::Context& context) const override; private: mutable OperationType m_operationType; + bool m_allowDowngrade; }; // Install or upgrade a single package @@ -62,4 +63,4 @@ namespace AppInstaller::CLI::Workflow private: mutable OperationType m_operationType; }; -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "WorkflowBase.h" #include "ExecutionContext.h" -#include "ManifestComparator.h" +#include <winget/ManifestComparator.h> #include "PromptFlow.h" #include "Sixel.h" #include "TableOutput.h" @@ -460,6 +460,57 @@ namespace AppInstaller::CLI::Workflow return HandleException(&context, exception); } + AppInstaller::Manifest::ManifestComparator::Options GetManifestComparatorOptions(const Execution::Context& context, const IPackageVersion::Metadata& metadata) + { + AppInstaller::Manifest::ManifestComparator::Options options; + bool getAllowedArchitecturesFromMetadata = false; + + if (context.Contains(Execution::Data::AllowedArchitectures)) + { + // Com caller can directly set allowed architectures + options.AllowedArchitectures = context.Get<Execution::Data::AllowedArchitectures>(); + } + else if (context.Args.Contains(Execution::Args::Type::InstallArchitecture)) + { + // Arguments provided in command line + options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallArchitecture))); + } + else if (context.Args.Contains(Execution::Args::Type::InstallerArchitecture)) + { + // Arguments provided in command line. Also skips applicability check. + options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallerArchitecture))); + options.SkipApplicabilityCheck = true; + } + else + { + getAllowedArchitecturesFromMetadata = true; + } + + if (context.Args.Contains(Execution::Args::Type::InstallerType)) + { + options.RequestedInstallerType = Manifest::ConvertToInstallerTypeEnum(std::string(context.Args.GetArg(Execution::Args::Type::InstallerType))); + } + + if (context.Args.Contains(Execution::Args::Type::InstallScope)) + { + options.RequestedInstallerScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + } + + if (context.Contains(Execution::Data::AllowUnknownScope)) + { + options.AllowUnknownScope = context.Get<Execution::Data::AllowUnknownScope>(); + } + + if (context.Args.Contains(Execution::Args::Type::Locale)) + { + options.RequestedInstallerLocale = context.Args.GetArg(Execution::Args::Type::Locale); + } + + Repository::GetManifestComparatorOptionsFromMetadata(options, metadata, getAllowedArchitecturesFromMetadata); + + return options; + } + void OpenSource::operator()(Execution::Context& context) const { std::string_view sourceName; @@ -1374,12 +1425,12 @@ namespace AppInstaller::CLI::Workflow installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); } - ManifestComparator manifestComparator(context, installationMetadata); + Manifest::ManifestComparator manifestComparator(GetManifestComparatorOptions(context, installationMetadata)); auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>()); if (!installer.has_value()) { - auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), InapplicabilityFlags::InstalledType); + auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), Manifest::InapplicabilityFlags::InstalledType); if (onlyInstalledType != inapplicabilities.end()) { if (isRepair) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -7,6 +7,7 @@ #include <winget/RepositorySearch.h> #include <winget/RepositorySource.h> #include <winget/Authentication.h> +#include <winget/ManifestComparator.h> #include <string> #include <string_view> @@ -91,6 +92,9 @@ namespace AppInstaller::CLI::Workflow // Helper to report exceptions and return the HRESULT. HRESULT HandleException(Execution::Context& context, std::exception_ptr exception); + // Fills the options from the given context and metadata. + AppInstaller::Manifest::ManifestComparator::Options GetManifestComparatorOptions(const Execution::Context& context, const Repository::IPackageVersion::Metadata& metadata); + // Creates the source object. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs b/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs @@ -0,0 +1,677 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ResourceCommands.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.Json; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// `Configure` command tests. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public class DSCv3ResourceCommands + { + private const string DefaultPackageIdentifier = Constants.ExeInstallerPackageId; + private const string DefaultPackageLowVersion = "1.0.0.0"; + private const string DefaultPackageMidVersion = "1.1.0.0"; + private const string DefaultPackageHighVersion = "2.0.0.0"; + private const string PackageResource = "package"; + private const string GetFunction = "get"; + private const string TestFunction = "test"; + private const string SetFunction = "set"; + private const string ExportFunction = "export"; + private const string ExistPropertyName = "_exist"; + private const string VersionPropertyName = "version"; + private const string UseLatestPropertyName = "useLatest"; + + /// <summary> + /// Ensures that the test resources manifests are present. + /// </summary> + public static void EnsureTestResourcePresence() + { + string outputDirectory = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\WindowsApps"); + Assert.IsNotEmpty(outputDirectory); + + var result = TestCommon.RunAICLICommand("dscv3 package", $"--manifest -o {outputDirectory}\\microsoft.winget.package.dsc.resource.json"); + Assert.AreEqual(0, result.ExitCode); + } + + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + TestCommon.SetupTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + WinGetSettingsHelper.ConfigureLoggingLevel("verbose"); + EnsureTestResourcePresence(); + } + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + RemoveTestPackage(); + WinGetSettingsHelper.ConfigureLoggingLevel(null); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + TestCommon.TearDownTestSource(); + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void Setup() + { + // Try clean up TestExeInstaller for failure cases where cleanup is not successful + RemoveTestPackage(); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value not present. + /// </summary> + [Test] + public void Package_Get_NotPresent() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `get` on the `package` resource with the package not existing. + /// </summary> + [Test] + public void Package_Get_UnknownIdentifier() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = "Not.An.Existing.Identifier.123456789.ABCDEFG" }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value present. + /// </summary> + [Test] + public void Package_Get_Present() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value present and supplying most inputs. + /// </summary> + [Test] + public void Package_Get_MuchInput() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Source = Constants.TestSourceName, + MatchOption = "equals", + }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + } + + /// <summary> + /// Calls `test` on the `package` resource with the value not present. + /// </summary> + [Test] + public void Package_Test_NotPresent() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + Assert.False(output.InDesiredState); + + Assert.IsNotNull(diff); + Assert.AreEqual(1, diff.Count); + Assert.AreEqual(ExistPropertyName, diff[0]); + } + + /// <summary> + /// Calls `test` on the `package` resource with the value present. + /// </summary> + [Test] + public void Package_Test_SimplePresent() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that matches. + /// </summary> + [Test] + public void Package_Test_VersionMatch() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageLowVersion, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that does not match. + /// </summary> + [Test] + public void Package_Test_VersionMismatch() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ VersionPropertyName ]); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that is the latest. + /// </summary> + [Test] + public void Package_Test_Latest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that is not the latest. + /// </summary> + [Test] + public void Package_Test_NotLatest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ UseLatestPropertyName ]); + } + + /// <summary> + /// Calls `set` on the `package` resource when the package is not present, and again afterward. + /// </summary> + [Test] + public void Package_Set_SimpleRepeated() + { + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Set again should be a no-op + result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (output, diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `set` on the `package` resource to ensure that it is not present. + /// </summary> + [Test] + public void Package_Set_Remove() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Exist = false, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceDataForGet.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `set` on the `package` resource to request the latest version when a lower version is installed. + /// </summary> + [Test] + public void Package_Set_Latest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); + + AssertDiffState(diff, [ UseLatestPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + } + + /// <summary> + /// Calls `set` on the `package` resource to request a specific version when a lower version is installed. + /// </summary> + [Test] + public void Package_Set_SpecificVersionUpgrade() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); + + AssertDiffState(diff, [ VersionPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + } + + /// <summary> + /// Calls `set` on the `package` resource to request a specific version when a higher version is installed. + /// </summary> + [Test] + public void Package_Set_SpecificVersionDowngrade() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); + + AssertDiffState(diff, [ VersionPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + } + + /// <summary> + /// Calls `export` on the `package` resource without providing any input. + /// </summary> + [Test] + public void Package_Export_NoInput() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + var result = RunDSCv3Command(PackageResource, ExportFunction, " "); + AssertSuccessfulResourceRun(ref result); + + List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); + + bool foundDefaultPackage = false; + foreach (PackageResourceData item in output) + { + if (item.Identifier == DefaultPackageIdentifier) + { + foundDefaultPackage = true; + Assert.IsNull(item.Version); + break; + } + } + + Assert.IsTrue(foundDefaultPackage); + } + + /// <summary> + /// Calls `export` on the `package` resource providing input to request that versions be included. + /// </summary> + [Test] + public void Package_Export_RequestVersions() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + UseLatest = false, + }; + + var result = RunDSCv3Command(PackageResource, ExportFunction, packageResourceData, 300000); + AssertSuccessfulResourceRun(ref result); + + List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); + + bool foundDefaultPackage = false; + foreach (PackageResourceData item in output) + { + if (item.Identifier == DefaultPackageIdentifier) + { + foundDefaultPackage = true; + Assert.AreEqual(DefaultPackageLowVersion, item.Version); + } + else + { + Assert.IsNotNull(item.Version); + Assert.IsNotEmpty(item.Version); + } + } + + Assert.IsTrue(foundDefaultPackage); + } + + private static void RemoveTestPackage() + { + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Exist = false, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + } + + private static TestCommon.RunCommandResult RunDSCv3Command(string resource, string function, object input, int timeOut = 60000, bool throwOnTimeout = true) + { + return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout); + } + + private static void AssertSuccessfulResourceRun(ref TestCommon.RunCommandResult result) + { + Assert.AreEqual(0, result.ExitCode); + Assert.IsNotEmpty(result.StdOut); + } + + private static JsonSerializerOptions GetDefaultJsonOptions() + { + return new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + } + + private static string ConvertToJSON(object value) => value switch + { + string s => s, + null => null, + _ => JsonSerializer.Serialize(value, GetDefaultJsonOptions()), + }; + + private static string[] GetOutputLines(string output) + { + return output.TrimEnd().Split(Environment.NewLine); + } + + private static T GetSingleOutputLineAs<T>(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(1, lines.Length); + + return JsonSerializer.Deserialize<T>(lines[0], GetDefaultJsonOptions()); + } + + private static (T, List<string>) GetSingleOutputLineAndDiffAs<T>(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(2, lines.Length); + + var options = GetDefaultJsonOptions(); + return (JsonSerializer.Deserialize<T>(lines[0], options), JsonSerializer.Deserialize<List<string>>(lines[1], options)); + } + + private static List<T> GetOutputLinesAs<T>(string output) + { + List<T> result = new List<T>(); + string[] lines = GetOutputLines(output); + var options = GetDefaultJsonOptions(); + + foreach (string line in lines) + { + result.Add(JsonSerializer.Deserialize<T>(line, options)); + } + + return result; + } + + private static void AssertExistingPackageResourceData(PackageResourceData output, string version, bool ignoreLatest = false) + { + Assert.IsNotNull(output); + Assert.True(output.Exist); + Assert.AreEqual(DefaultPackageIdentifier, output.Identifier); + Assert.AreEqual(version, output.Version); + + if (!ignoreLatest) + { + if (version == DefaultPackageHighVersion) + { + Assert.True(output.UseLatest); + } + else + { + Assert.False(output.UseLatest); + } + } + } + + private static void AssertDiffState(List<string> diff, IList<string> expected) + { + Assert.IsNotNull(diff); + Assert.AreEqual(expected.Count, diff.Count); + + foreach (string item in expected) + { + Assert.Contains(item, diff); + } + } + + private class PackageResourceData + { + [JsonPropertyName(ExistPropertyName)] + public bool? Exist { get; set; } + + [JsonPropertyName("_inDesiredState")] + public bool? InDesiredState { get; set; } + + [JsonPropertyName("id")] + public string Identifier { get; set; } + + public string Source { get; set; } + + public string Version { get; set; } + + public string MatchOption { get; set; } + + public bool? UseLatest { get; set; } + + public string InstallMode { get; set; } + + public bool? AcceptAgreements { get; set; } + } + } +} diff --git a/src/AppInstallerCLIE2ETests/DownloadCommand.cs b/src/AppInstallerCLIE2ETests/DownloadCommand.cs @@ -55,7 +55,7 @@ namespace AppInstallerCLIE2ETests var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --version {packageVersion}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); - TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.User, PackageInstallerType.Exe); } /// <summary> @@ -67,7 +67,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.User, PackageInstallerType.Exe); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -115,16 +115,27 @@ namespace AppInstallerCLIE2ETests.Helpers /// <returns>The result of the command.</returns> public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000, bool throwOnTimeout = true) { + string correlationParameter = " --correlation " + Guid.NewGuid().ToString(); + + // Don't include correlation when the call has an option ending `--` value. + foreach (string part in parameters.Split(' ', StringSplitOptions.TrimEntries)) + { + if (part == "--") + { + correlationParameter = string.Empty; + } + } + string inputMsg = "AICLI path: " + TestSetup.Parameters.AICLIPath + " Command: " + command + - " Parameters: " + parameters + + " Parameters: " + parameters + correlationParameter + (string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) + " Timeout: " + timeOut; TestContext.Out.WriteLine($"Starting command run. {inputMsg}"); - return RunAICLICommandViaDirectProcess(command, parameters, stdIn, timeOut, throwOnTimeout); + return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -231,6 +231,26 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> + /// Configure the logging level for the settings. + /// </summary> + /// <param name="level">The logging level to set; null removes the value.</param> + public static void ConfigureLoggingLevel(string level) + { + JObject settingsJson = GetJsonSettingsObject("logging"); + + if (level == null) + { + settingsJson["logging"]["level"]?.Parent?.Remove(); + } + else + { + settingsJson["logging"]["level"] = new JValue(level); + } + + SetWingetSettings(settingsJson); + } + + /// <summary> /// Configure experimental features. /// </summary> /// <param name="settingsJson">The settings JSON object to modify.</param> diff --git a/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs b/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs @@ -118,7 +118,7 @@ namespace AppInstallerCLIE2ETests.Interop var packageVersion = "2.0.0.0"; Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); - TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.User, PackageInstallerType.Exe); } /// <summary> @@ -142,7 +142,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.User, PackageInstallerType.Exe); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -482,7 +482,7 @@ namespace AppInstallerCLIE2ETests.Interop public async Task InstallRequireUserScope() { // Find package - var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstallerNoScope"); // Configure installation var installOptions = this.TestFactory.CreateInstallOptions(); @@ -505,7 +505,7 @@ namespace AppInstallerCLIE2ETests.Interop public async Task InstallRequireUserScopeAndUnknown() { // Find package - var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstallerNoScope"); // Configure installation var installOptions = this.TestFactory.CreateInstallOptions(); @@ -725,4 +725,4 @@ namespace AppInstallerCLIE2ETests.Interop Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.0.1.0.yaml @@ -5,6 +5,7 @@ Publisher: AppInstallerTest License: Test Installers: - Arch: x86 + Scope: user Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe @@ -17,4 +18,18 @@ Installers: Language: /exeenus Log: /exelog <LOGPATH> InstallLocation: /InstallDir <INSTALLPATH> + - Arch: x86 + Scope: machine + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /Version 1.0.1.0 /UseHKLM + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.1.1.0.0.yaml @@ -5,6 +5,7 @@ Publisher: AppInstallerTest License: Test Installers: - Arch: x86 + Scope: user Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe @@ -17,4 +18,18 @@ Installers: Language: /exeenus Log: /exelog <LOGPATH> InstallLocation: /InstallDir <INSTALLPATH> + - Arch: x86 + Scope: machine + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /Version 1.1.0.0 /UseHKLM + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml @@ -5,6 +5,7 @@ Publisher: AppInstallerTest License: Test Installers: - Arch: x86 + Scope: user Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe @@ -17,4 +18,18 @@ Installers: Language: /exeenus Log: /exelog <LOGPATH> InstallLocation: /InstallDir <INSTALLPATH> + - Arch: x86 + Scope: machine + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /Version 2.0.0.0 /UseHKLM + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml @@ -5,6 +5,7 @@ Publisher: AppInstallerTest License: Test Installers: - Arch: x86 + Scope: user Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe @@ -17,4 +18,18 @@ Installers: Language: /exeenus Log: /LogFile <LOGPATH> InstallLocation: /InstallDir <INSTALLPATH> + - Arch: x86 + Scope: machine + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom /UseHKLM + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_NoScope.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_NoScope.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestExeInstallerNoScope +Name: TestExeInstallerNoScope +Version: 1.0.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + Switches: + Custom: /execustom + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3247,4 +3247,43 @@ Please specify one of them using the --source option to proceed.</value> <data name="DscResourceFunctionDescriptionManifest" xml:space="preserve"> <value>Get the resource manifest</value> </data> + <data name="DscPackageResourceShortDescription" xml:space="preserve"> + <value>Manage package state</value> + </data> + <data name="DscPackageResourceLongDescription" xml:space="preserve"> + <value>Allows management of package state via the DSC v3 command line interface protocol. See the help link for details.</value> + </data> + <data name="DscResourcePropertyDescriptionExist" xml:space="preserve"> + <value>Indicates whether an instance should or does exist.</value> + </data> + <data name="DscResourcePropertyDescriptionInDesiredState" xml:space="preserve"> + <value>Indicates whether an instance is in the desired state.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageId" xml:space="preserve"> + <value>The identifier of the package.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageSource" xml:space="preserve"> + <value>The source of the package.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageVersion" xml:space="preserve"> + <value>The version of the package.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageMatchOption" xml:space="preserve"> + <value>The method for matching the identifier with a package.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageUseLatest" xml:space="preserve"> + <value>Indicate that the latest available version of the package should be installed.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageInstallMode" xml:space="preserve"> + <value>The install mode to use if needed.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageScope" xml:space="preserve"> + <value>The target scope of the package.</value> + </data> + <data name="DscResourcePropertyDescriptionPackageAcceptAgreements" xml:space="preserve"> + <value>Indicates whether to accept agreements for package installs.</value> + </data> + <data name="CorrelationArgumentDescription" xml:space="preserve"> + <value>Value is logged for correlation</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -149,9 +149,6 @@ <ClCompile Include="LanguageUtilities.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> - <ClCompile Include="ManifestComparator.cpp"> - <Filter>Source Files\CLI</Filter> - </ClCompile> <ClCompile Include="MSStoreDownloadFlow.cpp"> <Filter>Source Files\CLI</Filter> </ClCompile> @@ -377,6 +374,9 @@ <ClCompile Include="RestInterface_1_10.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="ManifestComparator.cpp"> + <Filter>Source Files\Common</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -1081,4 +1081,4 @@ <Filter>TestData</Filter> </CopyFileToFolders> </ItemGroup> -</Project> +</Project>+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -4,8 +4,9 @@ #include "TestCommon.h" #include <ExecutionContext.h> #include <COMContext.h> -#include <Workflows/ManifestComparator.h> +#include <winget/ManifestComparator.h> #include <winget/UserSettings.h> +#include <Workflows/WorkflowBase.h> using namespace std::string_literals; using namespace std::string_view_literals; @@ -91,7 +92,7 @@ TEST_CASE("ManifestComparator_OSFilter_Low", "[manifest_comparator]") Manifest manifest; AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.99999.0"); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -103,7 +104,7 @@ TEST_CASE("ManifestComparator_OSFilter_High", "[manifest_comparator]") Manifest manifest; ManifestInstaller expected = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.0.0"); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, expected); @@ -117,7 +118,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter_Unknown", "[manifest_comparat SECTION("Nothing Installed") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -129,7 +130,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter_Unknown", "[manifest_comparat IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::User); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, unknown); @@ -140,7 +141,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter_Unknown", "[manifest_comparat IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::Machine); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, unknown); @@ -156,7 +157,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter", "[manifest_comparator]") SECTION("Nothing Installed") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -168,7 +169,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::User); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, user); @@ -179,7 +180,7 @@ TEST_CASE("ManifestComparator_InstalledScopeFilter", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::Machine); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, machine); @@ -195,7 +196,7 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") SECTION("Nothing Installed") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -207,7 +208,7 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Msi); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msi); @@ -218,7 +219,7 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Msix); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msix); @@ -234,7 +235,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") SECTION("Nothing Installed") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -246,7 +247,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Exe); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, burn); @@ -257,7 +258,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Inno); - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, burn); @@ -273,7 +274,7 @@ TEST_CASE("ManifestComparator_ScopeFilter", "[manifest_comparator]") SECTION("Nothing Required") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -285,7 +286,7 @@ TEST_CASE("ManifestComparator_ScopeFilter", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::InstallScope, ScopeToString(ScopeEnum::User)); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, user); @@ -296,7 +297,7 @@ TEST_CASE("ManifestComparator_ScopeFilter", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::InstallScope, ScopeToString(ScopeEnum::Machine)); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, machine); @@ -312,7 +313,7 @@ TEST_CASE("ManifestComparator_ScopeCompare", "[manifest_comparator]") SECTION("No Preference") { - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // The default preference is user @@ -324,7 +325,7 @@ TEST_CASE("ManifestComparator_ScopeCompare", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallScopePreference>(ScopeEnum::User); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, user); @@ -335,7 +336,7 @@ TEST_CASE("ManifestComparator_ScopeCompare", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallScopePreference>(ScopeEnum::Machine); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, machine); @@ -354,7 +355,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed_WithUnknown", "[manifes TestUserSettings settings; settings.Set<Setting::InstallLocalePreference>({ "en-US" }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -366,7 +367,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed_WithUnknown", "[manifes IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledLocale] = "en-US"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, enGB); @@ -377,7 +378,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed_WithUnknown", "[manifes IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledLocale] = "zh-CN"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, unknown); @@ -396,7 +397,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed", "[manifest_comparator TestUserSettings settings; settings.Set<Setting::InstallLocalePreference>({ "en-US" }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); // Only because it is first @@ -408,7 +409,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed", "[manifest_comparator IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledLocale] = "en-US"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, enGB); @@ -419,7 +420,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed", "[manifest_comparator IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledLocale] = "zh-CN"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -431,7 +432,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed", "[manifest_comparator metadata[PackageVersionMetadata::InstalledLocale] = "en-US"; metadata[PackageVersionMetadata::UserIntentLocale] = "fr-FR"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, frFR); @@ -443,7 +444,7 @@ TEST_CASE("ManifestComparator_LocaleComparator_Installed", "[manifest_comparator metadata[PackageVersionMetadata::InstalledLocale] = "en-US"; metadata[PackageVersionMetadata::UserIntentLocale] = "zh-CN"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -463,7 +464,7 @@ TEST_CASE("ManifestComparator_LocaleComparator", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::Locale, "en-GB"s); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, enGB); @@ -474,7 +475,7 @@ TEST_CASE("ManifestComparator_LocaleComparator", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::Locale, "zh-CN"s); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -485,7 +486,7 @@ TEST_CASE("ManifestComparator_LocaleComparator", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallLocalePreference>({ "en-US" }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, enGB); @@ -496,7 +497,7 @@ TEST_CASE("ManifestComparator_LocaleComparator", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallLocalePreference>({ "zh-CN" }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, unknown); @@ -512,7 +513,7 @@ TEST_CASE("ManifestComparator_AllowedArchitecture_x64_only", "[manifest_comparat ManifestComparatorTestContext context; context.Add<Data::AllowedArchitectures>({ Architecture::X64 }); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -532,7 +533,7 @@ TEST_CASE("ManifestComparator_AllowedArchitecture", "[manifest_comparator]") ManifestComparatorTestContext context; context.Add<Data::AllowedArchitectures>({ Architecture::X86, Architecture::X64 }); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, x86); @@ -543,7 +544,7 @@ TEST_CASE("ManifestComparator_AllowedArchitecture", "[manifest_comparator]") ManifestComparatorTestContext context; context.Add<Data::AllowedArchitectures>({ Architecture::Unknown }); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(result); @@ -555,7 +556,7 @@ TEST_CASE("ManifestComparator_AllowedArchitecture", "[manifest_comparator]") ManifestComparatorTestContext context; context.Add<Data::AllowedArchitectures>({ Architecture::X86, Architecture::Unknown }); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, x86); @@ -574,7 +575,7 @@ TEST_CASE("ManifestComparator_Architectures_WithUserIntent", "[manifest_comparat IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledArchitecture] = "x86"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, x86); @@ -586,7 +587,7 @@ TEST_CASE("ManifestComparator_Architectures_WithUserIntent", "[manifest_comparat metadata[PackageVersionMetadata::InstalledArchitecture] = "x86"; metadata[PackageVersionMetadata::UserIntentArchitecture] = "x64"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, x64); @@ -601,7 +602,7 @@ TEST_CASE("ManifestComparator_Architectures_WithUserIntent", "[manifest_comparat metadata[PackageVersionMetadata::InstalledArchitecture] = "x86"; metadata[PackageVersionMetadata::UserIntentArchitecture] = "x64"; - ManifestComparator mc(ManifestComparatorTestContext{}, metadata); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(x86OnlyManifest); REQUIRE(!result); @@ -631,7 +632,7 @@ TEST_CASE("ManifestComparator_UnsupportedOSArchitecture", "[manifest_comparator] { ManifestInstaller installer = AddInstaller(manifest, applicableArchitecture, InstallerTypeEnum::Msi, {}, {}, {}, { systemArchitecture }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -641,7 +642,7 @@ TEST_CASE("ManifestComparator_UnsupportedOSArchitecture", "[manifest_comparator] { ManifestInstaller installer = AddInstaller(manifest, applicableArchitecture, InstallerTypeEnum::Msi, {}, {}, {}, { applicableArchitecture }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, installer); @@ -662,7 +663,7 @@ TEST_CASE("ManifestComparator_Inapplicabilities", "[manifest_comparator]") IPackageVersion::Metadata metadata; metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Msix); - ManifestComparator mc(context, metadata); + ManifestComparator mc(GetManifestComparatorOptions(context, metadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -692,7 +693,7 @@ TEST_CASE("ManifestComparator_MarketFilter", "[manifest_comparator]") } ManifestInstaller installer = AddInstaller(manifest, Architecture::X86, InstallerTypeEnum::Exe, {}, {}, {}, {}, markets); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, installer); @@ -712,7 +713,7 @@ TEST_CASE("ManifestComparator_MarketFilter", "[manifest_comparator]") } ManifestInstaller installer = AddInstaller(manifest, Architecture::X86, InstallerTypeEnum::Exe, {}, {}, {}, {}, markets); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -730,7 +731,7 @@ TEST_CASE("ManifestComparator_Scope_AllowUnknown", "[manifest_comparator]") SECTION("Default") { - ManifestComparator mc(testContext, {}); + ManifestComparator mc(GetManifestComparatorOptions(testContext, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -740,7 +741,7 @@ TEST_CASE("ManifestComparator_Scope_AllowUnknown", "[manifest_comparator]") { testContext.Add<Data::AllowUnknownScope>(true); - ManifestComparator mc(testContext, {}); + ManifestComparator mc(GetManifestComparatorOptions(testContext, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, expected); @@ -760,7 +761,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::InstallerType, "msi"s); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msi); @@ -771,7 +772,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::InstallerType, "msix"s); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msix); @@ -782,7 +783,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") ManifestComparatorTestContext context; context.Args.AddArg(Args::Type::InstallerType, "portable"s); - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -793,7 +794,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Exe }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, exe); @@ -804,7 +805,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Portable }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msi); @@ -815,7 +816,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, exe); @@ -826,7 +827,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypePreference>({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, msix); @@ -837,7 +838,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Exe }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, exe); @@ -848,7 +849,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") TestUserSettings settings; settings.Set<Setting::InstallerTypeRequirement>({ InstallerTypeEnum::Inno }); - ManifestComparator mc(ManifestComparatorTestContext{}, {}); + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); REQUIRE(!result); @@ -864,7 +865,7 @@ TEST_CASE("ManifestComparator_MachineArchitecture_Strong_Scope_Weak", "[manifest ManifestComparatorTestContext context; - ManifestComparator mc(context, {}); + ManifestComparator mc(GetManifestComparatorOptions(context, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, system); @@ -881,7 +882,7 @@ TEST_CASE("ManifestComparator_InstallerCompatibilitySet_Weaker_Than_Architecture IPackageVersion::Metadata installationMetadata; installationMetadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(foil.EffectiveInstallerType()); - ManifestComparator mc(context, installationMetadata); + ManifestComparator mc(GetManifestComparatorOptions(context, installationMetadata)); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); RequireInstaller(result, target); diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -348,6 +348,7 @@ <ClInclude Include="Public\winget\Authentication.h" /> <ClInclude Include="Public\winget\FileCache.h" /> <ClInclude Include="Public\winget\FolderFileWatcher.h" /> + <ClInclude Include="Public\winget\ManifestComparator.h" /> <ClInclude Include="Public\winget\MsixManifest.h" /> <ClInclude Include="Public\winget\AdminSettings.h" /> <ClInclude Include="Public\winget\Debugging.h" /> @@ -402,6 +403,7 @@ <ClInclude Include="Public\winget\ManifestSchemaValidation.h" /> <ClInclude Include="Public\winget\SelfManagement.h" /> <ClInclude Include="Public\winget\Settings.h" /> + <ClInclude Include="Public\winget\StdErrLogger.h" /> <ClInclude Include="Public\winget\ThreadGlobals.h" /> <ClInclude Include="Public\winget\TraceLogger.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> @@ -431,6 +433,7 @@ <ClCompile Include="Locale.cpp" /> <ClCompile Include="Manifest\Manifest.cpp" /> <ClCompile Include="Manifest\ManifestCommon.cpp" /> + <ClCompile Include="Manifest\ManifestComparator.cpp" /> <ClCompile Include="Manifest\ManifestValidation.cpp" /> <ClCompile Include="Manifest\ManifestSchemaValidation.cpp" /> <ClCompile Include="Manifest\ManifestYamlPopulator.cpp" /> @@ -466,6 +469,7 @@ <ClCompile Include="Settings.cpp" /> <ClCompile Include="PathVariable.cpp" /> <ClCompile Include="PortableARPEntry.cpp" /> + <ClCompile Include="StdErrLogger.cpp" /> <ClCompile Include="Synchronization.cpp" /> <ClCompile Include="Telemetry\TraceLogging.cpp" /> <ClCompile Include="Architecture.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -210,6 +210,12 @@ <ClInclude Include="Public\winget\OutputDebugStringLogger.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\ManifestComparator.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\StdErrLogger.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -380,6 +386,12 @@ <ClCompile Include="OutputDebugStringLogger.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Manifest\ManifestComparator.cpp"> + <Filter>Manifest</Filter> + </ClCompile> + <ClCompile Include="StdErrLogger.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -0,0 +1,897 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include <winget/ManifestComparator.h> +#include <AppInstallerLogging.h> +#include <winget/UserSettings.h> +#include <winget/Runtime.h> +#include <winget/Locale.h> + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Manifest +{ + std::ostream& operator<<(std::ostream& out, const ManifestInstaller& installer) + { + return out << '[' << + AppInstaller::Utility::ToString(installer.Arch) << ',' << + AppInstaller::Manifest::InstallerTypeToString(installer.EffectiveInstallerType()) << ',' << + AppInstaller::Manifest::ScopeToString(installer.Scope) << ',' << + installer.Locale << ']'; + } +} + +namespace AppInstaller::Manifest +{ + namespace + { + struct PortableInstallFilter : public details::FilterField + { + PortableInstallFilter() : details::FilterField("Portable Install") {} + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + // Unvirtualized resources restricted capability is only supported for >= 10.0.18362 + // TODO: Add support for OS versions that don't support virtualization. + if (installer.EffectiveInstallerType() == InstallerTypeEnum::Portable && !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version("10.0.18362"))) + { + return InapplicabilityFlags::OSVersion; + } + + return InapplicabilityFlags::None; + } + + std::string ExplainInapplicable(const ManifestInstaller&) override + { + std::string result = "Current OS is lower than supported MinOSVersion (10.0.18362) for Portable install"; + return result; + } + }; + + struct OSVersionFilter : public details::FilterField + { + OSVersionFilter() : details::FilterField("OS Version") {} + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + if (installer.MinOSVersion.empty() || Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion))) + { + return InapplicabilityFlags::None; + } + + return InapplicabilityFlags::OSVersion; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Current OS is lower than MinOSVersion "; + result += installer.MinOSVersion; + return result; + } + }; + + struct MachineArchitectureComparator : public details::ComparisonField + { + MachineArchitectureComparator() : details::ComparisonField("Machine Architecture") {} + + MachineArchitectureComparator(std::vector<Utility::Architecture> allowedArchitectures) : + details::ComparisonField("Machine Architecture"), m_allowedArchitectures(std::move(allowedArchitectures)) + { + AICLI_LOG(CLI, Verbose, << "Architecture Comparator created with allowed architectures: " << Utility::ConvertContainerToString(m_allowedArchitectures, Utility::ToString)); + } + + static std::unique_ptr<MachineArchitectureComparator> Create(const ManifestComparator::Options& options) + { + if (!options.AllowedArchitectures.empty()) + { + // If the incoming data contains elements, we will use them to construct a final allowed list. + // The algorithm is to take elements until we find Unknown, which indicates that any architecture is + // acceptable at this point. The system supported set of architectures will then be placed at the end. + std::vector<Utility::Architecture> result; + bool addRemainingApplicableArchitectures = false; + + for (Utility::Architecture architecture : options.AllowedArchitectures) + { + if (architecture == Utility::Architecture::Unknown) + { + addRemainingApplicableArchitectures = true; + break; + } + + // If the architecture is applicable and not already in our result set... + if ((options.SkipApplicabilityCheck || Utility::IsApplicableArchitecture(architecture) != Utility::InapplicableArchitecture) && + Utility::IsApplicableArchitecture(architecture, result) == Utility::InapplicableArchitecture) + { + result.push_back(architecture); + } + } + + if (addRemainingApplicableArchitectures) + { + for (Utility::Architecture architecture : Utility::GetApplicableArchitectures()) + { + if (Utility::IsApplicableArchitecture(architecture, result) == Utility::InapplicableArchitecture) + { + result.push_back(architecture); + } + } + } + + return std::make_unique<MachineArchitectureComparator>(std::move(result)); + } + else + { + return std::make_unique<MachineArchitectureComparator>(); + } + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + if (CheckAllowedArchitecture(installer.Arch) == Utility::InapplicableArchitecture || + IsSystemArchitectureUnsupportedByInstaller(installer)) + { + return InapplicabilityFlags::MachineArchitecture; + } + + return InapplicabilityFlags::None; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result; + if (Utility::IsApplicableArchitecture(installer.Arch) == Utility::InapplicableArchitecture) + { + result = "Machine is not compatible with "; + result += Utility::ToString(installer.Arch); + } + else if (IsSystemArchitectureUnsupportedByInstaller(installer)) + { + result = "System architecture is unsupported by installer"; + } + else + { + result = "Architecture was excluded by caller : "; + result += Utility::ToString(installer.Arch); + } + + return result; + } + + details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override + { + auto arch1 = CheckAllowedArchitecture(first.Arch); + auto arch2 = CheckAllowedArchitecture(second.Arch); + + if (arch1 > arch2) + { + // A match with the primary architecture is strong + return (first.Arch == GetStrongArchitectureMatch() ? details::ComparisonResult::StrongPositive : details::ComparisonResult::WeakPositive); + } + + return details::ComparisonResult::Negative; + } + + private: + int CheckAllowedArchitecture(Utility::Architecture architecture) + { + if (m_allowedArchitectures.empty()) + { + return Utility::IsApplicableArchitecture(architecture); + } + else + { + return Utility::IsApplicableArchitecture(architecture, m_allowedArchitectures); + } + } + + bool IsSystemArchitectureUnsupportedByInstaller(const ManifestInstaller& installer) + { + auto unsupportedItr = std::find( + installer.UnsupportedOSArchitectures.begin(), + installer.UnsupportedOSArchitectures.end(), + Utility::GetSystemArchitecture()); + return unsupportedItr != installer.UnsupportedOSArchitectures.end(); + } + + Utility::Architecture GetStrongArchitectureMatch() + { + // If we have a preferential order, treat the first entry as strong. + // Otherwise, treat the system architecture as strong (which is always first in the default order). + return m_allowedArchitectures.empty() ? Utility::GetSystemArchitecture() : m_allowedArchitectures.front(); + } + + std::vector<Utility::Architecture> m_allowedArchitectures; + }; + + struct InstallerTypeComparator : public details::ComparisonField + { + 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 + << " , Preferred InstallerTypes: " << m_preferenceAsString); + } + + static std::unique_ptr<InstallerTypeComparator> Create(const ManifestComparator::Options& options) + { + std::vector<InstallerTypeEnum> preference; + std::vector<InstallerTypeEnum> requirement; + + if (options.RequestedInstallerType) + { + requirement.emplace_back(options.RequestedInstallerType.value()); + } + else + { + preference = Settings::User().Get<Settings::Setting::InstallerTypePreference>(); + requirement = Settings::User().Get<Settings::Setting::InstallerTypeRequirement>(); + } + + if (!preference.empty() || !requirement.empty()) + { + return std::make_unique<InstallerTypeComparator>(preference, requirement); + } + else + { + return {}; + } + } + + std::string ExplainInapplicable(const 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 ManifestInstaller& installer) override + { + if (!m_requirement.empty()) + { + // The installer is applicable if the effective or base installer type matches. + if (ContainsInstallerType(m_requirement, installer.EffectiveInstallerType()) || + ContainsInstallerType(m_requirement, installer.BaseInstallerType)) + { + return InapplicabilityFlags::None; + } + + return InapplicabilityFlags::InstallerType; + } + else + { + return InapplicabilityFlags::None; + } + } + + details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override + { + if (m_preference.empty()) + { + return details::ComparisonResult::Negative; + } + + for (InstallerTypeEnum installerTypePreference : m_preference) + { + bool isFirstInstallerTypePreferred = + first.EffectiveInstallerType() == installerTypePreference || + first.BaseInstallerType == installerTypePreference; + + bool isSecondInstallerTypePreferred = + second.EffectiveInstallerType() == installerTypePreference || + second.BaseInstallerType == installerTypePreference; + + if (isFirstInstallerTypePreferred && isSecondInstallerTypePreferred) + { + return details::ComparisonResult::Negative; + } + else if (isFirstInstallerTypePreferred != isSecondInstallerTypePreferred) + { + // Treating this as a weak positive because one can use requirements to guarantee the installer type if necessary. + return (isFirstInstallerTypePreferred ? details::ComparisonResult::WeakPositive : details::ComparisonResult::Negative); + } + } + + return details::ComparisonResult::Negative; + } + + 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 InstalledTypeFilter : public details::FilterField + { + InstalledTypeFilter(InstallerTypeEnum installedType) : + details::FilterField("Installed Type"), m_installedType(installedType) {} + + static std::unique_ptr<InstalledTypeFilter> Create(const ManifestComparator::Options& options) + { + if (options.CurrentlyInstalledType) + { + InstallerTypeEnum installedType = options.CurrentlyInstalledType.value(); + if (installedType != InstallerTypeEnum::Unknown) + { + return std::make_unique<InstalledTypeFilter>(installedType); + } + } + + return {}; + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + return IsInstallerCompatibleWith(installer, m_installedType) ? InapplicabilityFlags::None : InapplicabilityFlags::InstalledType; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Installed package type '" + std::string{ InstallerTypeToString(m_installedType) } + + "' is not compatible with installer type " + std::string{ InstallerTypeToString(installer.EffectiveInstallerType()) }; + + std::string arpInstallerTypes; + for (const auto& entry : installer.AppsAndFeaturesEntries) + { + arpInstallerTypes += " " + std::string{ InstallerTypeToString(entry.InstallerType) }; + } + + if (!arpInstallerTypes.empty()) + { + result += ", or with accepted type(s)" + arpInstallerTypes; + } + + return result; + } + + private: + // The installer is compatible if it's type or any of its ARP entries' type matches the installed type + static bool IsInstallerCompatibleWith(const ManifestInstaller& installer, InstallerTypeEnum type) + { + if (IsInstallerTypeCompatible(installer.EffectiveInstallerType(), type)) + { + return true; + } + + auto itr = std::find_if( + installer.AppsAndFeaturesEntries.begin(), + installer.AppsAndFeaturesEntries.end(), + [=](AppsAndFeaturesEntry arpEntry) { return IsInstallerTypeCompatible(arpEntry.InstallerType, type); }); + if (itr != installer.AppsAndFeaturesEntries.end()) + { + return true; + } + + return false; + } + + InstallerTypeEnum m_installedType; + }; + + struct InstalledScopeFilter : public details::FilterField + { + InstalledScopeFilter(ScopeEnum requirement) : + details::FilterField("Installed Scope"), m_requirement(requirement) {} + + static std::unique_ptr<InstalledScopeFilter> Create(const ManifestComparator::Options& options) + { + // Check for an existing install and require a matching scope. + if (options.CurrentlyInstalledScope) + { + ScopeEnum installedScope = options.CurrentlyInstalledScope.value(); + if (installedScope != ScopeEnum::Unknown) + { + return std::make_unique<InstalledScopeFilter>(installedScope); + } + } + + return {}; + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + // We have to assume the unknown scope will match our required scope, or the entire catalog would stop working for upgrade. + if (installer.Scope == ScopeEnum::Unknown || installer.Scope == m_requirement || DoesInstallerTypeIgnoreScopeFromManifest(installer.EffectiveInstallerType())) + { + return InapplicabilityFlags::None; + } + + return InapplicabilityFlags::InstalledScope; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Installer scope does not match currently installed scope: "; + result += ScopeToString(installer.Scope); + result += " != "; + result += ScopeToString(m_requirement); + return result; + } + + private: + ScopeEnum m_requirement; + }; + + struct ScopeComparator : public details::ComparisonField + { + ScopeComparator(ScopeEnum preference, ScopeEnum requirement, bool allowUnknownInAdditionToRequired) : + details::ComparisonField("Scope"), m_preference(preference), m_requirement(requirement), m_allowUnknownInAdditionToRequired(allowUnknownInAdditionToRequired) {} + + static std::unique_ptr<ScopeComparator> Create(const ManifestComparator::Options& options) + { + // Preference will always come from settings + ScopeEnum preference = Settings::User().Get<Settings::Setting::InstallScopePreference>(); + + // Requirement may come from args or settings; args overrides settings. + ScopeEnum requirement = ScopeEnum::Unknown; + + if (options.RequestedInstallerScope) + { + requirement = options.RequestedInstallerScope.value(); + } + else + { + requirement = Settings::User().Get<Settings::Setting::InstallScopeRequirement>(); + } + + bool allowUnknownInAdditionToRequired = false; + if (options.AllowUnknownScope) + { + allowUnknownInAdditionToRequired = options.AllowUnknownScope.value(); + + // Force the required type to be preferred over Unknown + if (requirement != ScopeEnum::Unknown) + { + preference = requirement; + } + } + + if (preference != ScopeEnum::Unknown || requirement != ScopeEnum::Unknown) + { + return std::make_unique<ScopeComparator>(preference, requirement, allowUnknownInAdditionToRequired); + } + else + { + return {}; + } + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + // 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 == ScopeEnum::Unknown || + installer.Scope == m_requirement || + (installer.Scope == ScopeEnum::Unknown && m_allowUnknownInAdditionToRequired) || + DoesInstallerTypeIgnoreScopeFromManifest(installer.EffectiveInstallerType())) + { + return InapplicabilityFlags::None; + } + + return InapplicabilityFlags::Scope; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Installer scope does not match required scope: "; + result += ScopeToString(installer.Scope); + result += " != "; + result += ScopeToString(m_requirement); + return result; + } + + details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override + { + if (m_preference != ScopeEnum::Unknown && first.Scope == m_preference && second.Scope != m_preference) + { + // When the second input is unknown, this is a weak result. If it is not (and therefore the opposite of the preference), this is strong. + return (second.Scope == ScopeEnum::Unknown ? details::ComparisonResult::WeakPositive : details::ComparisonResult::StrongPositive); + } + + return details::ComparisonResult::Negative; + } + + private: + ScopeEnum m_preference; + ScopeEnum m_requirement; + bool m_allowUnknownInAdditionToRequired; + }; + + struct LocaleComparator : public details::ComparisonField + { + LocaleComparator(std::vector<std::string> preference, std::vector<std::string> requirement, bool isInstalledLocale) : + details::ComparisonField("Locale"), m_preference(std::move(preference)), m_requirement(std::move(requirement)), m_isInstalledLocale(isInstalledLocale) + { + m_requirementAsString = Utility::ConvertContainerToString(m_requirement); + m_preferenceAsString = Utility::ConvertContainerToString(m_preference); + AICLI_LOG(CLI, Verbose, + << "Locale Comparator created with Required Locales: " << m_requirementAsString + << " , Preferred Locales: " << m_preferenceAsString + << " , IsInstalledLocale: " << m_isInstalledLocale); + } + + static std::unique_ptr<LocaleComparator> Create(const ManifestComparator::Options& options) + { + std::vector<std::string> preference; + std::vector<std::string> requirement; + // This is for installed locale case, where the locale is a preference but requires at least compatible match. + bool isInstalledLocale = false; + + // Requirement may come from args, previous user intent or settings; args overrides previous user intent then settings. + if (options.RequestedInstallerLocale) + { + requirement.emplace_back(options.RequestedInstallerLocale.value()); + } + else if (options.PreviousUserIntentLocale) + { + requirement.emplace_back(options.PreviousUserIntentLocale.value()); + isInstalledLocale = true; + } + else + { + if (!options.CurrentlyInstalledLocale) + { + // If it's an upgrade of previous package, no need to set requirements from settings + // as previous installed locale will be used later. + requirement = Settings::User().Get<Settings::Setting::InstallLocaleRequirement>(); + } + } + + // Preference will come from previous installed locale, winget settings or Preferred Languages settings. + // Previous installed locale goes first, then winget settings, then Preferred Languages settings. + // Previous installed locale also requires at least compatible locale match. + if (options.CurrentlyInstalledLocale) + { + preference.emplace_back(options.CurrentlyInstalledLocale.value()); + isInstalledLocale = true; + } + else + { + preference = Settings::User().Get<Settings::Setting::InstallLocalePreference>(); + if (preference.empty()) + { + preference = Locale::GetUserPreferredLanguages(); + } + } + + if (!preference.empty() || !requirement.empty()) + { + return std::make_unique<LocaleComparator>(preference, requirement, isInstalledLocale); + } + else + { + return {}; + } + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + InapplicabilityFlags inapplicableFlag = m_isInstalledLocale ? InapplicabilityFlags::InstalledLocale : InapplicabilityFlags::Locale; + + if (!m_requirement.empty()) + { + // Check if requirement is satisfied + for (auto const& requiredLocale : m_requirement) + { + if (Locale::GetDistanceOfLanguage(requiredLocale, installer.Locale) >= Locale::MinimumDistanceScoreAsPerfectMatch) + { + return InapplicabilityFlags::None; + } + } + + return inapplicableFlag; + } + else if (m_isInstalledLocale && !m_preference.empty()) + { + // For installed locale preference, check at least compatible match for preference + for (auto const& preferredLocale : m_preference) + { + // We have to assume an unknown installer locale will match our installed locale, or the entire catalog would stop working for upgrade. + if (installer.Locale.empty() || + Locale::GetDistanceOfLanguage(preferredLocale, installer.Locale) >= Locale::MinimumDistanceScoreAsCompatibleMatch) + { + return InapplicabilityFlags::None; + } + } + + return inapplicableFlag; + } + else + { + return InapplicabilityFlags::None; + } + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Installer locale does not match required locale: "; + result += installer.Locale; + result += "Required locales: "; + result += m_requirementAsString; + result += " Or does not satisfy compatible match for Preferred Locales: "; + result += m_preferenceAsString; + return result; + } + + details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override + { + if (m_preference.empty()) + { + return details::ComparisonResult::Negative; + } + + for (auto const& preferredLocale : m_preference) + { + double firstScore = first.Locale.empty() ? Locale::UnknownLanguageDistanceScore : Locale::GetDistanceOfLanguage(preferredLocale, first.Locale); + double secondScore = second.Locale.empty() ? Locale::UnknownLanguageDistanceScore : Locale::GetDistanceOfLanguage(preferredLocale, second.Locale); + + if (firstScore >= Locale::MinimumDistanceScoreAsCompatibleMatch || secondScore >= Locale::MinimumDistanceScoreAsCompatibleMatch) + { + // This could probably be enriched to always check all locales and determine strong/weak based off of the MinimumDistanceScoreAsCompatibleMatch. + return (firstScore > secondScore ? details::ComparisonResult::StrongPositive : details::ComparisonResult::Negative); + } + } + + // At this point, the installer locale matches no preference. + // if first is unknown and second is no match for sure, we might prefer unknown one. + return (first.Locale.empty() && !second.Locale.empty() ? details::ComparisonResult::WeakPositive : details::ComparisonResult::Negative); + } + + private: + std::vector<std::string> m_preference; + std::vector<std::string> m_requirement; + std::string m_requirementAsString; + std::string m_preferenceAsString; + bool m_isInstalledLocale = false; + }; + + struct MarketFilter : public details::FilterField + { + MarketFilter(Manifest::string_t market) : details::FilterField("Market"), m_market(market) + { + AICLI_LOG(CLI, Verbose, << "Market Filter created with market: " << m_market); + } + + static std::unique_ptr<MarketFilter> Create() + { + return std::make_unique<MarketFilter>(Runtime::GetOSRegion()); + } + + InapplicabilityFlags IsApplicable(const ManifestInstaller& installer) override + { + // If both allowed and excluded lists are provided, we only need to check the allowed markets. + if (!installer.Markets.AllowedMarkets.empty()) + { + // Inapplicable if NOT found + if (!IsMarketInList(installer.Markets.AllowedMarkets)) + { + return InapplicabilityFlags::Market; + } + } + else if (!installer.Markets.ExcludedMarkets.empty()) + { + // Inapplicable if found + if (IsMarketInList(installer.Markets.ExcludedMarkets)) + { + return InapplicabilityFlags::Market; + } + } + + return InapplicabilityFlags::None; + } + + std::string ExplainInapplicable(const ManifestInstaller& installer) override + { + std::string result = "Current market '" + m_market + "' does not match installer markets." + + " Allowed markets: " + Utility::ConvertContainerToString(installer.Markets.AllowedMarkets) + + " Excluded markets: " + Utility::ConvertContainerToString(installer.Markets.ExcludedMarkets); + return result; + } + + private: + bool IsMarketInList(const std::vector<Manifest::string_t> markets) + { + return markets.end() != std::find_if( + markets.begin(), + markets.end(), + [&](const auto& m) { return Utility::CaseInsensitiveEquals(m, m_market); }); + } + + Manifest::string_t m_market; + }; + } + + 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 + AddFilter(InstalledScopeFilter::Create(options)); + // Filters based on the market region of the system + AddFilter(MarketFilter::Create()); + // Filters based on the installer type compatability, including with AppsAndFeaturesEntry declarations + AddFilter(InstalledTypeFilter::Create(options)); + + // Filter order is not important, but comparison order determines priority. + // Note that all comparators are also filters and their comparison function will only be called on + // installers that both match the required criteria. + // + // The comparators are ordered by the `IsFirstBetter` method, which uses the following algorithm: + // - Each comparison between two installers can return one of { Strong, Weak, Negative } + // - Installers are compared in both directions, going through the list of comparators as defined here + // - The first Strong result in either direction is given priority + // - If no Strong results, the first Weak result is used + // - If all Negative results, then the two installers are equal in priority (meaning the first one in the list is kept as "better") + // + // TODO: There are improvements to be made here around ordering, especially in the context of implicit vs explicit vs command line preferences. + + // Filters based on exact matches for requirements or compatible matches for preferences + // Only applies when preference exists: + // Strong if first is compatible and better match than second + // Weak if first is unknown and second is not + AddComparator(LocaleComparator::Create(options)); + // Filters only if a requirement is present and it cannot be satisfied by the installer (including installer types that we can control scope in code) + // Only applies when preference exists: + // Strong if first matches preference and second does not and is not Unknown + // Weak if first matches preference and second is Unknown + AddComparator(ScopeComparator::Create(options)); + // Filters architectures out that are not supported or are not in the preferences/requirements/inputs. + // Strong if first equals the earliest architecture in the allowed list and second does not [default means the system architecture] + // Weak if first is better match for system architecture than second + AddComparator(MachineArchitectureComparator::Create(options)); + // Filters installer types out that are not in preferences or requirements. + // Only applies when preference exists: + // Weak if first is in preference list and second is not + AddComparator(InstallerTypeComparator::Create(options)); + } + + InstallerAndInapplicabilities ManifestComparator::GetPreferredInstaller(const Manifest& manifest) + { + AICLI_LOG(CLI, Verbose, << "Starting installer selection."); + + const ManifestInstaller* result = nullptr; + std::vector<InapplicabilityFlags> inapplicabilitiesInstallers; + + for (const auto& installer : manifest.Installers) + { + auto inapplicabilityInstaller = IsApplicable(installer); + if (inapplicabilityInstaller == InapplicabilityFlags::None) + { + if (!result || IsFirstBetter(installer, *result)) + { + AICLI_LOG(CLI, Verbose, << "Installer " << installer << " is current best choice"); + result = &installer; + } + } + else + { + inapplicabilitiesInstallers.push_back(inapplicabilityInstaller); + } + } + + if (!result) + { + return { {}, std::move(inapplicabilitiesInstallers) }; + } + + return { *result, std::move(inapplicabilitiesInstallers) }; + } + + InapplicabilityFlags ManifestComparator::IsApplicable(const ManifestInstaller& installer) + { + InapplicabilityFlags inapplicabilityResult = InapplicabilityFlags::None; + + for (const auto& filter : m_filters) + { + auto inapplicability = filter->IsApplicable(installer); + if (inapplicability != InapplicabilityFlags::None) + { + AICLI_LOG(CLI, Verbose, << "Installer " << installer << " not applicable: " << filter->ExplainInapplicable(installer)); + WI_SetAllFlags(inapplicabilityResult, inapplicability); + } + } + + return inapplicabilityResult; + } + + bool ManifestComparator::IsFirstBetter( + const ManifestInstaller& first, + const ManifestInstaller& second) + { + // The priority will still be used as a tie-break between weak results. + std::optional<std::string_view> firstWeakComparator; + bool firstWeakComparatorResult = false; + + for (auto comparator : m_comparators) + { + details::ComparisonResult forwardCompare = comparator->IsFirstBetter(first, second); + details::ComparisonResult reverseCompare = comparator->IsFirstBetter(second, first); + + // Should not happen, but if it does it points at a serious bug that should be fixed. + if (forwardCompare != details::ComparisonResult::Negative && reverseCompare != details::ComparisonResult::Negative) + { + AICLI_LOG(CLI, Error, << "Installer " << first << " and " << second << " are both better than each other?"); + THROW_HR(E_UNEXPECTED); + } + + if (forwardCompare == details::ComparisonResult::StrongPositive) + { + AICLI_LOG(CLI, Verbose, << "Installer " << first << " is better [strong] than " << second << " due to: " << comparator->Name()); + return true; + } + + if (reverseCompare == details::ComparisonResult::StrongPositive) + { + // Second is better by this comparator, don't allow a lower priority one to override that. + AICLI_LOG(CLI, Verbose, << "Installer " << second << " is better [strong] than " << first << " due to: " << comparator->Name()); + return false; + } + + // Save the first weak result that we get + if (!firstWeakComparator) + { + if (forwardCompare == details::ComparisonResult::WeakPositive) + { + firstWeakComparator = comparator->Name(); + firstWeakComparatorResult = true; + } + else if (reverseCompare == details::ComparisonResult::WeakPositive) + { + firstWeakComparator = comparator->Name(); + firstWeakComparatorResult = false; + } + } + } + + // If we found a weak result (and no strong result because we made it here), return it. + if (firstWeakComparator) + { + if (firstWeakComparatorResult) + { + AICLI_LOG(CLI, Verbose, << "Installer " << first << " is better [weak] than " << second << " due to: " << *firstWeakComparator); + } + else + { + AICLI_LOG(CLI, Verbose, << "Installer " << second << " is better [weak] than " << first << " due to: " << *firstWeakComparator); + } + + return firstWeakComparatorResult; + } + + // Equal, and thus not better + AICLI_LOG(CLI, Verbose, << "Installer " << first << " and " << second << " are equivalent in priority"); + return false; + } + + void ManifestComparator::AddFilter(std::unique_ptr<details::FilterField>&& filter) + { + if (filter) + { + m_filters.emplace_back(std::move(filter)); + } + } + + void ManifestComparator::AddComparator(std::unique_ptr<details::ComparisonField>&& comparator) + { + if (comparator) + { + m_comparators.push_back(comparator.get()); + m_filters.emplace_back(std::move(comparator)); + } + } +} diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestComparator.h b/src/AppInstallerCommonCore/Public/winget/ManifestComparator.h @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/Manifest.h> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + + +namespace AppInstaller::Manifest +{ + // Flags to indicate why an installer was not applicable + enum class InapplicabilityFlags : int + { + None = 0x0, + OSVersion = 0x1, + InstalledScope = 0x2, + InstalledType = 0x4, + InstalledLocale = 0x8, + Locale = 0x10, + Scope = 0x20, + MachineArchitecture = 0x40, + Market = 0x80, + InstallerType = 0x100, + }; + + DEFINE_ENUM_FLAG_OPERATORS(InapplicabilityFlags); + + namespace details + { + // An interface for defining new filters based on user inputs. + struct FilterField + { + FilterField(std::string_view name) : m_name(name) {} + + virtual ~FilterField() = default; + + std::string_view Name() const { return m_name; } + + // Determines if the installer is applicable based on this field alone. + virtual InapplicabilityFlags IsApplicable(const AppInstaller::Manifest::ManifestInstaller& installer) = 0; + + // Explains why the filter regarded this installer as inapplicable. + // Will only be called when IsApplicable returns false. + virtual std::string ExplainInapplicable(const AppInstaller::Manifest::ManifestInstaller& installer) = 0; + + private: + std::string_view m_name; + }; + + // The result of ComparisonField::IsFirstBetter + enum class ComparisonResult + { + // The first input is not better than the second input. + Negative, + // The first input is somewhat better than the second input. + // If another comparison has a strong positive result, it will override a weak result. + WeakPositive, + // The first input is definitely better than the second input. + StrongPositive, + }; + + // An interface for defining new comparisons based on user inputs. + struct ComparisonField : public FilterField + { + using FilterField::FilterField; + + virtual ~ComparisonField() = default; + + // Determines if the first installer is a better choice based on this field alone. + virtual ComparisonResult IsFirstBetter(const AppInstaller::Manifest::ManifestInstaller& first, const AppInstaller::Manifest::ManifestInstaller& second) = 0; + }; + } + + struct InstallerAndInapplicabilities + { + std::optional<AppInstaller::Manifest::ManifestInstaller> installer; + std::vector<InapplicabilityFlags> inapplicabilitiesInstaller; + }; + + // Class in charge of comparing manifest entries + struct ManifestComparator + { + // Options that affect the comparisons. + struct Options + { + // The allowed architectures and a value indicating whether to perform applicability checks. + std::vector<Utility::Architecture> AllowedArchitectures; + bool SkipApplicabilityCheck = false; + + // The requested installer type. + std::optional<InstallerTypeEnum> RequestedInstallerType; + + // The currently installed type. + std::optional<InstallerTypeEnum> CurrentlyInstalledType; + + // The requested installer scope and a value indicating whether and unknown scope is acceptable. + std::optional<ScopeEnum> RequestedInstallerScope; + std::optional<bool> AllowUnknownScope; + + // The currently installed scope. + std::optional<ScopeEnum> CurrentlyInstalledScope; + + // The requested installer locale. + std::optional<std::string> RequestedInstallerLocale; + + // Get the currently installed locale intent and value. + std::optional<std::string> PreviousUserIntentLocale; + std::optional<std::string> CurrentlyInstalledLocale; + }; + + ManifestComparator(const Options& options); + + // Gets the best installer from the manifest, if at least one is applicable. + InstallerAndInapplicabilities GetPreferredInstaller(const AppInstaller::Manifest::Manifest& manifest); + + // Determines if an installer is applicable. + InapplicabilityFlags IsApplicable(const AppInstaller::Manifest::ManifestInstaller& installer); + + // Determines if the first installer is a better choice. + bool IsFirstBetter( + const AppInstaller::Manifest::ManifestInstaller& first, + const AppInstaller::Manifest::ManifestInstaller& second); + + private: + void AddFilter(std::unique_ptr<details::FilterField>&& filter); + void AddComparator(std::unique_ptr<details::ComparisonField>&& comparator); + + std::vector<std::unique_ptr<details::FilterField>> m_filters; + // Non-owning pointers to values in m_filters. + std::vector<details::ComparisonField*> m_comparators; + }; + +} diff --git a/src/AppInstallerCommonCore/Public/winget/StdErrLogger.h b/src/AppInstallerCommonCore/Public/winget/StdErrLogger.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerLogging.h> + +namespace AppInstaller::Logging +{ + // Sends logs to the stderr stream. + struct StdErrLogger : ILogger + { + StdErrLogger() = default; + + ~StdErrLogger() = default; + + // ILogger + std::string GetName() const override; + + void Write(Channel channel, Level level, std::string_view message) noexcept override; + + void WriteDirect(Channel channel, Level level, std::string_view message) noexcept override; + + // Adds OutputDebugStringLogger to the current Log + static void Add(); + + // Removes OutputDebugStringLogger from the current Log + static void Remove(); + + private: + Level m_level = Level::Error; + }; +} diff --git a/src/AppInstallerCommonCore/StdErrLogger.cpp b/src/AppInstallerCommonCore/StdErrLogger.cpp @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/StdErrLogger.h" + +namespace AppInstaller::Logging +{ + namespace + { + static constexpr std::string_view s_StdErrLoggerName = "StdErrLogger"; + } + + std::string StdErrLogger::GetName() const + { + return std::string{ s_StdErrLoggerName }; + } + + void StdErrLogger::Write(Channel channel, Level level, std::string_view message) noexcept try + { + if (level >= m_level) + { + std::cerr << "[" << std::setw(GetMaxChannelNameLength()) << std::left << std::setfill(' ') << GetChannelName(channel) << "] " << message << std::endl; + } + } + catch (...) + { + // Just eat any exceptions here; better than losing logs + } + + void StdErrLogger::WriteDirect(Channel, Level level, std::string_view message) noexcept try + { + if (level >= m_level) + { + std::cerr.write(message.data(), static_cast<std::streamsize>(message.size())); + } + } + catch (...) + { + // Just eat any exceptions here; better than losing logs + } + + void StdErrLogger::Add() + { + Log().AddLogger(std::make_unique<StdErrLogger>()); + } + + void StdErrLogger::Remove() + { + Log().RemoveLogger(std::string{ s_StdErrLoggerName }); + } +} diff --git a/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp b/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Public/winget/PackageVersionSelection.h" #include "Public/winget/RepositorySource.h" +#include "Public/winget/PinningData.h" namespace AppInstaller::Repository @@ -154,4 +155,125 @@ namespace AppInstaller::Repository { return GetAvailablePackageFromSource(composite->GetAvailable(), sourceIdentifier); } + + LatestApplicableVersionData GetLatestApplicableVersion(const std::shared_ptr<ICompositePackage>& composite) + { + using namespace AppInstaller::Pinning; + + LatestApplicableVersionData result; + + auto installedVersion = AppInstaller::Repository::GetInstalledVersion(composite); + auto availableVersions = AppInstaller::Repository::GetAvailableVersionsForInstalledVersion(composite, installedVersion); + + PinningData pinningData{ PinningData::Disposition::ReadOnly }; + auto evaluator = pinningData.CreatePinStateEvaluator(PinBehavior::ConsiderPins, installedVersion); + + AppInstaller::Manifest::ManifestComparator::Options options; + if (installedVersion) + { + GetManifestComparatorOptionsFromMetadata(options, installedVersion->GetMetadata()); + } + AppInstaller::Manifest::ManifestComparator manifestComparator{ options }; + + auto availableVersionKeys = availableVersions->GetVersionKeys(); + for (const auto& availableVersionKey : availableVersionKeys) + { + auto availableVersion = availableVersions->GetVersion(availableVersionKey); + + if (installedVersion && !evaluator.IsUpdate(availableVersion)) + { + // Version too low or different channel for upgrade + continue; + } + + if (evaluator.EvaluatePinType(availableVersion) != AppInstaller::Pinning::PinType::Unknown) + { + // Pinned + continue; + } + + auto manifestComparatorResult = manifestComparator.GetPreferredInstaller(availableVersion->GetManifest()); + if (!manifestComparatorResult.installer.has_value()) + { + // No applicable installer + continue; + } + + result.LatestApplicableVersion = availableVersion; + if (installedVersion) + { + result.UpdateAvailable = true; + } + + break; + } + + return result; + } + + void GetManifestComparatorOptionsFromMetadata(AppInstaller::Manifest::ManifestComparator::Options& options, const IPackageVersion::Metadata& metadata, bool includeAllowedArchitectures) + { + auto installedTypeItr = metadata.find(Repository::PackageVersionMetadata::InstalledType); + if (installedTypeItr != metadata.end()) + { + options.CurrentlyInstalledType = Manifest::ConvertToInstallerTypeEnum(installedTypeItr->second); + } + + auto installedScopeItr = metadata.find(Repository::PackageVersionMetadata::InstalledScope); + if (installedScopeItr != metadata.end()) + { + options.CurrentlyInstalledScope = Manifest::ConvertToScopeEnum(installedScopeItr->second); + } + + auto userIntentLocaleItr = metadata.find(Repository::PackageVersionMetadata::UserIntentLocale); + if (userIntentLocaleItr != metadata.end()) + { + options.PreviousUserIntentLocale = userIntentLocaleItr->second; + } + + auto installedLocaleItr = metadata.find(Repository::PackageVersionMetadata::InstalledLocale); + if (installedLocaleItr != metadata.end()) + { + options.CurrentlyInstalledLocale = installedLocaleItr->second; + } + + if (includeAllowedArchitectures) + { + auto userIntentItr = metadata.find(Repository::PackageVersionMetadata::UserIntentArchitecture); + if (userIntentItr != metadata.end()) + { + // For upgrade, user intent from previous install is considered requirement + options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(userIntentItr->second)); + } + else + { + auto installedItr = metadata.find(Repository::PackageVersionMetadata::InstalledArchitecture); + if (installedItr != metadata.end()) + { + // For upgrade, previous installed architecture should be considered first preference and is always allowed. + // Then check settings requirements and preferences. + options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(installedItr->second)); + } + + std::vector<Utility::Architecture> requiredArchitectures = Settings::User().Get<Settings::Setting::InstallArchitectureRequirement>(); + std::vector<Utility::Architecture> optionalArchitectures = Settings::User().Get<Settings::Setting::InstallArchitecturePreference>(); + + if (!requiredArchitectures.empty()) + { + // Required architecture list from settings if applicable + options.AllowedArchitectures.insert(options.AllowedArchitectures.end(), requiredArchitectures.begin(), requiredArchitectures.end()); + } + else + { + // Preferred architecture list from settings if applicable, add Unknown to indicate allowing remaining applicable + if (!optionalArchitectures.empty()) + { + options.AllowedArchitectures.insert(options.AllowedArchitectures.end(), optionalArchitectures.begin(), optionalArchitectures.end()); + } + + options.AllowedArchitectures.emplace_back(Utility::Architecture::Unknown); + } + } + } + } } diff --git a/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h b/src/AppInstallerRepositoryCore/Public/winget/PackageVersionSelection.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include <winget/RepositorySearch.h> +#include <winget/ManifestComparator.h> namespace AppInstaller::Repository @@ -25,4 +26,16 @@ namespace AppInstaller::Repository // Gets the available IPackage corresponding to the given source identifier. std::shared_ptr<IPackage> GetAvailablePackageFromSource(const std::shared_ptr<ICompositePackage>& composite, const std::string_view sourceIdentifier); + + struct LatestApplicableVersionData + { + std::shared_ptr<AppInstaller::Repository::IPackageVersion> LatestApplicableVersion; + bool UpdateAvailable = false; + }; + + // Determines the default install version and whether an update is available. + LatestApplicableVersionData GetLatestApplicableVersion(const std::shared_ptr<ICompositePackage>& composite); + + // Fills the options from the given metadata, optionally including the allowed architectures. + void GetManifestComparatorOptionsFromMetadata(AppInstaller::Manifest::ManifestComparator::Options& options, const IPackageVersion::Metadata& metadata, bool includeAllowedArchitectures = true); } diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -303,7 +303,7 @@ void WriteToUninstallRegistry( } // Set ModifyPath Property Value - if (LONG res = RegSetValueEx(hkey, L"ModifyPath", NULL, REG_EXPAND_SZ, (LPBYTE)modifyPath.c_str(), (DWORD)(modifyPath.wstring().length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + if (LONG res = RegSetValueEx(hkey, L"ModifyPath", NULL, REG_EXPAND_SZ, (LPBYTE)modifyPathString.c_str(), (DWORD)(modifyPathString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { out << "Failed to write ModifyPath value. Error Code: " << res << std::endl; } diff --git a/src/Microsoft.Management.Deployment/CatalogPackage.cpp b/src/Microsoft.Management.Deployment/CatalogPackage.cpp @@ -11,7 +11,6 @@ #include "PackageInstallerInstalledStatus.h" #include "CheckInstalledStatusResult.h" #include <ComContext.h> -#include <Workflows/ManifestComparator.h> #include <wil\cppwinrt_wrl.h> #include <winget/PinningData.h> #include <winget/PackageVersionSelection.h> @@ -72,60 +71,17 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::call_once(m_latestApplicableVersionOnceFlag, [&]() { - using namespace AppInstaller::Pinning; + auto data = AppInstaller::Repository::GetLatestApplicableVersion(m_package); - auto availableVersions = AppInstaller::Repository::GetAvailableVersionsForInstalledVersion(m_package); - auto installedVersion = AppInstaller::Repository::GetInstalledVersion(m_package); + m_updateAvailable = data.UpdateAvailable; - PinningData pinningData{ PinningData::Disposition::ReadOnly }; - auto evaluator = pinningData.CreatePinStateEvaluator(PinBehavior::ConsiderPins, installedVersion); - - AppInstaller::CLI::Execution::COMContext context; - AppInstaller::Repository::IPackageVersion::Metadata installationMetadata = - installedVersion ? installedVersion->GetMetadata() : AppInstaller::Repository::IPackageVersion::Metadata{}; - AppInstaller::CLI::Workflow::ManifestComparator manifestComparator{ context, installationMetadata }; - - std::shared_ptr<AppInstaller::Repository::IPackageVersion> latestApplicableVersion; - auto availableVersionKeys = availableVersions->GetVersionKeys(); - for (const auto& availableVersionKey : availableVersionKeys) - { - auto availableVersion = availableVersions->GetVersion(availableVersionKey); - - if (installedVersion && !evaluator.IsUpdate(availableVersion)) - { - // Version too low or different channel for upgrade - continue; - } - - if (evaluator.EvaluatePinType(availableVersion) != AppInstaller::Pinning::PinType::Unknown) - { - // Pinned - continue; - } - - auto manifestComparatorResult = manifestComparator.GetPreferredInstaller(availableVersion->GetManifest()); - if (!manifestComparatorResult.installer.has_value()) - { - // No applicable installer - continue; - } - - latestApplicableVersion = availableVersion; - if (installedVersion) - { - m_updateAvailable = true; - } - - break; - } - - if (latestApplicableVersion) + if (data.LatestApplicableVersion) { // DefaultInstallVersion hasn't been created yet, create and populate it. // DefaultInstallVersion is the latest applicable version of the internal package object. auto latestVersionImpl = winrt::make_self<wil::details::module_count_wrapper< winrt::Microsoft::Management::Deployment::implementation::PackageVersionInfo>>(); - latestVersionImpl->Initialize(std::move(latestApplicableVersion)); + latestVersionImpl->Initialize(std::move(data.LatestApplicableVersion)); m_latestApplicableVersion = *latestVersionImpl; } }); @@ -141,7 +97,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation } else if (AvailableVersions().Size() > 0) { - return GetPackageVersionInfo(AvailableVersions().GetAt(0)); + return GetPackageVersionInfo(m_availableVersions.GetAt(0)); } else { diff --git a/src/Microsoft.Management.Deployment/PackageVersionInfo.cpp b/src/Microsoft.Management.Deployment/PackageVersionInfo.cpp @@ -12,7 +12,7 @@ #include "CatalogPackageMetadata.h" #include "ComContext.h" #include "Workflows/WorkflowBase.h" -#include "Workflows/ManifestComparator.h" +#include <winget/ManifestComparator.h> #include "winget/RepositorySearch.h" #include "AppInstallerVersions.h" #include "Converters.h" @@ -26,6 +26,42 @@ namespace winrt::Microsoft::Management::Deployment::implementation { + namespace + { + // Do the same thing as PopulateContextFromInstallOptions but without all the string conversions. + AppInstaller::Manifest::ManifestComparator::Options GetComparatorOptionsFromInstallOptions(InstallOptions options) + { + AppInstaller::Manifest::ManifestComparator::Options result; + + auto installerType = GetManifestInstallerType(options.InstallerType()); + if (installerType != AppInstaller::Manifest::InstallerTypeEnum::Unknown) + { + result.RequestedInstallerType = installerType; + } + + auto manifestScope = GetManifestScope(options.PackageInstallScope()); + if (manifestScope.first != ::AppInstaller::Manifest::ScopeEnum::Unknown) + { + result.RequestedInstallerScope = manifestScope.first; + result.AllowUnknownScope = manifestScope.second; + } + + if (options.AllowedArchitectures().Size() != 0) + { + for (auto architecture : options.AllowedArchitectures()) + { + auto convertedArchitecture = GetUtilityArchitecture(architecture); + if (convertedArchitecture) + { + result.AllowedArchitectures.push_back(convertedArchitecture.value()); + } + } + } + + return result; + } + } + void PackageVersionInfo::Initialize(std::shared_ptr<::AppInstaller::Repository::IPackageVersion> packageVersion) { m_packageVersion = std::move(packageVersion); @@ -138,20 +174,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation } bool PackageVersionInfo::HasApplicableInstaller(InstallOptions options) { - AppInstaller::CLI::Execution::COMContext context; - PopulateContextFromInstallOptions(&context, options); - AppInstaller::Repository::IPackageVersion::Metadata installationMetadata; - AppInstaller::CLI::Workflow::ManifestComparator manifestComparator{ context, installationMetadata }; + AppInstaller::Manifest::ManifestComparator manifestComparator{ GetComparatorOptionsFromInstallOptions(options) }; AppInstaller::Manifest::Manifest manifest = m_packageVersion->GetManifest(); auto result = manifestComparator.GetPreferredInstaller(manifest); return result.installer.has_value(); } winrt::Microsoft::Management::Deployment::PackageInstallerInfo PackageVersionInfo::GetApplicableInstaller(InstallOptions options) { - AppInstaller::CLI::Execution::COMContext context; - PopulateContextFromInstallOptions(&context, options); - AppInstaller::Repository::IPackageVersion::Metadata installationMetadata; - AppInstaller::CLI::Workflow::ManifestComparator manifestComparator{ context, installationMetadata }; + AppInstaller::Manifest::ManifestComparator manifestComparator{ GetComparatorOptionsFromInstallOptions(options) }; AppInstaller::Manifest::Manifest manifest = m_packageVersion->GetManifest(); auto result = manifestComparator.GetPreferredInstaller(manifest);