commit 2fb627db6710e29f45d09cb327f0aa79176f049b parent f84764d953774de58cea69fdfe6fca9cc903a25d Author: JohnMcPMS <johnmcp@microsoft.com> Date: Thu, 1 Apr 2021 09:27:49 -0700 Settings and command line argument for specifying scope (#819) This change adds settings and a command line argument to `install` to enable control over the scope of the package install. Scope is the difference between a user or machine install of a package. The settings allow both the preference and requirement of scope to be set. A preference affects the sort order for selecting installers, while a requirement filters the installers. The default behavior (with no settings configured) is equivalent to: ```json "installBehavior": { "preferences": { "scope": "user" } }, ``` The command line argument is `--scope` and accepts the values `user` and `machine`. This is treated as a requirement, overriding any requirement set in the settings file. The default value for Scope in a manifest is being changed from implicitly `User` to just `Unknown`. If a requirement is specified (which cannot be `Unknown`), it will not match the default value of `Unknown`. This is necessary to ensure that we can meet the requirement. It will also affect what is a valid manifest; an unknown scope value will now conflict with any other scope value to prevent specifying an unknown and known value together. Finally, to implement the actual behavior change, `ManifestComparator` is completely rewritten to be a more dynamic configuration of filters and comparators. This will allow for more behavior to be added in the future in a straightforward manner. It also fixes a bug that previously existed where the sorting was not consistent (ie. would not produce a properly sorted list). It will now only use a lower priority sort when the higher priority sort considers the two installers equal. Diffstat:
36 files changed, 1132 insertions(+), 192 deletions(-)
diff --git a/doc/Settings.md b/doc/Settings.md @@ -12,7 +12,7 @@ If you are using the non-packaged WinGet version by building it from source code The `source` settings involve configuration to the WinGet source. -``` +```json "source": { "autoUpdateIntervalInMinutes": 3 }, @@ -31,7 +31,7 @@ To manually update the source use `winget source update` The `visual` settings involve visual elements that are displayed by WinGet -``` +```json "visual": { "progressBar": "accent" }, @@ -45,6 +45,28 @@ Color of the progress bar that WinGet displays when not specified by arguments. - retro - rainbow +## Install Behavior + +The `installBehavior` settings affect the default behavior of installing and upgrading (where applicable) packages. + +### Preferences and Requirements + +Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. + +Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. + +### Scope + +The `scope` behavior affects the choice between installing a package for the current user or for the entire machine. The matching parameter is `--scope`, and uses the same values (`user` or `machine`). + +```json + "installBehavior": { + "preferences": { + "scope": "user" + } + }, +``` + ## Telemetry The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. @@ -67,7 +89,7 @@ To allow work to be done and distributed to early adopters for feedback, setting The `experimentalFeatures` settings involve the configuration of these "experimental" features. Individual features can be enabled under this node. The example below shows sample experimental features. -``` +```json "experimentalFeatures": { "experimentalCmd": true, "experimentalArg": false @@ -78,7 +100,7 @@ The `experimentalFeatures` settings involve the configuration of these "experime Microsoft Store App support in WinGet is currently implemented as an experimental feature. It supports a curated list of utility apps from Microsoft Store. You can enable the feature as shown below. -``` +```json "experimentalFeatures": { "experimentalMSStore": true }, @@ -88,7 +110,7 @@ Microsoft Store App support in WinGet is currently implemented as an experimenta While work is in progress on list, the command is hidden behind a feature toggle. One can enable it as below: -``` +```json "experimentalFeatures": { "list": true }, @@ -98,7 +120,7 @@ While work is in progress on list, the command is hidden behind a feature toggle While work is in progress on upgrade, the command is hidden behind a feature toggle. One can enable it as below: -``` +```json "experimentalFeatures": { "upgrade": true }, @@ -108,7 +130,7 @@ While work is in progress on upgrade, the command is hidden behind a feature tog While work is in progress on uninstall, the command is hidden behind a feature toggle. One can enable it as below: -``` +```json "experimentalFeatures": { "uninstall": true }, @@ -118,7 +140,7 @@ While work is in progress on uninstall, the command is hidden behind a feature t While work is in progress for import, the command is hidden behind a feature toggle. One can enable it as below: -``` +```json "experimentalFeatures": { "import": true }, @@ -128,7 +150,7 @@ While work is in progress for import, the command is hidden behind a feature tog While work is in progress for rest source support, the feature is hidden behind a feature toggle. Enabling this will not change how client works currently and will allow testing any additional rest sources added. One can enable it as below: -``` +```json "experimentalFeatures": { "restSource": true }, diff --git a/schemas/JSON/packages/packages.schema.1.0.json b/schemas/JSON/packages/packages.schema.1.0.json @@ -86,6 +86,16 @@ "Channel": { "description": "Package channel", "type": "string" + }, + + "Scope": { + "description": "Required package scope", + "type": "string", + "enum": [ + "user", + "machine" + ], + "default": "user" } } } diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -32,6 +32,28 @@ } } }, + "InstallPrefReq": { + "description": "Shared schema for preferences and requirements", + "type": "object", + "properties": { + "scope": { + "description": "The scope of a package install", + "type": "string", + "enum": [ + "user", "machine" + ], + "default": "user" + } + } + }, + "InstallBehavior": { + "description": "Install settings", + "type": "object", + "properties": { + "preferences": { "$ref": "#/definitions/InstallPrefReq"}, + "requirements": { "$ref": "#/definitions/InstallPrefReq"} + } + }, "Telemetry": { "description": "Telemetry settings", "type": "object", @@ -105,16 +127,22 @@ }, { "properties": { + "installBehavior": { "$ref": "#/definitions/InstallBehavior"} + }, + "additionalItems": true + }, + { + "properties": { "telemetry": { "$ref": "#/definitions/Telemetry"} }, "additionalItems": true }, { - "properties": { - "experimentalFeatures": { "$ref": "#/definitions/Experimental"} - }, - "additionalItems": true - } + "properties": { + "experimentalFeatures": { "$ref": "#/definitions/Experimental"} + }, + "additionalItems": true + } ], "additionalProperties": true } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -78,6 +78,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "spelling", "spelling", "{2A EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cpprestsdk", "cpprestsdk\cpprestsdk.vcxproj", "{866C3F06-636F-4BE8-BC24-5F86ECC606A1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "schemas", "schemas", "{92637527-6CDA-4F4A-84FD-858793776777}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JSON", "JSON", "{F2149997-295A-4593-9282-4C675DFEB670}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "settings", "settings", "{1487DFBB-7C53-4BD3-9B2C-9B94C6C91528}" + ProjectSection(SolutionItems) = preProject + ..\schemas\JSON\settings\settings.schema.0.2.json = ..\schemas\JSON\settings\settings.schema.0.2.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{F5CED6B6-C27F-4405-9033-6C273B8B129C}" + ProjectSection(SolutionItems) = preProject + ..\schemas\JSON\packages\packages.schema.1.0.json = ..\schemas\JSON\packages\packages.schema.1.0.json + EndProjectSection +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ManifestSchema\ManifestSchema.vcxitems*{1622da16-914f-4f57-a259-d5169003cc8c}*SharedItemsImports = 4 @@ -459,6 +473,10 @@ Global {952B513F-8A00-4D74-9271-925AFB3C6252} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {2ACDE176-F13F-42FA-8159-C34FA3D37837} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {866C3F06-636F-4BE8-BC24-5F86ECC606A1} = {60618CAC-2995-4DF9-9914-45C6FC02C995} + {92637527-6CDA-4F4A-84FD-858793776777} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {F2149997-295A-4593-9282-4C675DFEB670} = {92637527-6CDA-4F4A-84FD-858793776777} + {1487DFBB-7C53-4BD3-9B2C-9B94C6C91528} = {F2149997-295A-4593-9282-4C675DFEB670} + {F5CED6B6-C27F-4405-9033-6C273B8B129C} = {F2149997-295A-4593-9282-4C675DFEB670} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -12,6 +12,31 @@ using namespace AppInstaller::Settings; namespace AppInstaller::CLI { constexpr std::string_view s_Command_ArgName_SilentAndInteractive = "silent|interactive"sv; + constexpr std::string_view s_CommandException_ReplacementToken = "%1"sv; + + const Utility::LocIndString CommandException::Message() const + { + if (m_replace) + { + std::string result; + + // Find the %1 in the message + std::string_view message = m_message.get(); + size_t index = message.find(s_CommandException_ReplacementToken); + + if (index != std::string::npos) + { + result = message.substr(0, index); + result += m_replace.value(); + result += message.substr(index + s_CommandException_ReplacementToken.length()); + + return Utility::LocIndString{ std::move(result) }; + } + } + + // Fall back to just using the message. + return Utility::LocIndString{ m_message.get() }; + } Command::Command( std::string_view name, @@ -50,8 +75,28 @@ namespace AppInstaller::CLI // Error if given if (exception) { - reporter.Error() << - exception->Message() << " : '"_liv << exception->Param() << '\'' << std::endl << + auto error = reporter.Error(); + error << exception->Message(); + + if (!exception->Params().empty()) + { + error << " :"_liv; + bool first = true; + for (const auto& param : exception->Params()) + { + if (first) + { + first = false; + } + else + { + error << ','; + } + error << " '"_liv << param << '\''; + } + } + + error << std::endl << std::endl; } diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -11,6 +11,7 @@ #include <initializer_list> #include <memory> +#include <optional> #include <ostream> #include <string> #include <string_view> @@ -21,18 +22,26 @@ namespace AppInstaller::CLI { struct CommandException { + CommandException(Resource::LocString message) : m_message(std::move(message)) {} + // The message should be a localized string. // The parameters can be either localized or not. // We 'convert' the param to a localization independent view here if needed. - CommandException(Resource::LocString message, Resource::LocString param) : m_message(std::move(message)), m_param(param) {} - CommandException(Resource::LocString message, std::string_view param) : m_message(std::move(message)), m_param(param) {} + CommandException(Resource::LocString message, Resource::LocString param) : m_message(std::move(message)), m_params({ param }) {} + CommandException(Resource::LocString message, std::string_view param) : m_message(std::move(message)), m_params({ Utility::LocIndString{ param } }) {} + + // The message should be a localized string, but the replacement and parameters are not. + // This supports replacing %1 in the message with the replace value. + CommandException(Resource::LocString message, Utility::LocIndView replace, std::vector<Utility::LocIndString>&& params) : + m_message(std::move(message)), m_replace(replace), m_params(std::move(params)) {} - const Resource::LocString& Message() const { return m_message; } - const Utility::LocIndString& Param() const { return m_param; } + const Utility::LocIndString Message() const; + const std::vector<Utility::LocIndString>& Params() const { return m_params; } private: Resource::LocString m_message; - Utility::LocIndString m_param; + std::optional<Utility::LocIndString> m_replace; + std::vector<Utility::LocIndString> m_params; }; struct Command diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -8,11 +8,17 @@ #include "Resources.h" using namespace AppInstaller::CLI::Execution; -using namespace AppInstaller::Manifest; using namespace AppInstaller::CLI::Workflow; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI { + namespace + { + constexpr Utility::LocIndView s_ArgumentName_Scope = "scope"_liv; + } + std::vector<Argument> InstallCommand::GetArguments() const { return { @@ -24,6 +30,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Version), Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Source), + Argument{ s_ArgumentName_Scope, Argument::NoAlias, Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), @@ -45,29 +52,29 @@ namespace AppInstaller::CLI return { Resource::String::InstallCommandLongDescription }; } - void InstallCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + void InstallCommand::Complete(Context& context, Args::Type valueType) const { switch (valueType) { - case Execution::Args::Type::Query: - case Execution::Args::Type::Manifest: - case Execution::Args::Type::Id: - case Execution::Args::Type::Name: - case Execution::Args::Type::Moniker: - case Execution::Args::Type::Version: - case Execution::Args::Type::Channel: - case Execution::Args::Type::Source: + case Args::Type::Query: + case Args::Type::Manifest: + case Args::Type::Id: + case Args::Type::Name: + case Args::Type::Moniker: + case Args::Type::Version: + case Args::Type::Channel: + case Args::Type::Source: context << Workflow::CompleteWithSingleSemanticsForValue(valueType); break; - case Execution::Args::Type::Language: + case Args::Type::Language: // May well move to CompleteWithSingleSemanticsForValue, // but for now output nothing. context << Workflow::CompleteWithEmptySet; break; - case Execution::Args::Type::Log: - case Execution::Args::Type::InstallLocation: + case Args::Type::Log: + case Args::Type::InstallLocation: // Intentionally output nothing to allow pass through to filesystem. break; } @@ -78,23 +85,31 @@ namespace AppInstaller::CLI return "https://aka.ms/winget-command-install"; } - void InstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + void InstallCommand::ValidateArgumentsInternal(Args& execArgs) const { - if (execArgs.Contains(Execution::Args::Type::Manifest) && - (execArgs.Contains(Execution::Args::Type::Query) || - execArgs.Contains(Execution::Args::Type::Id) || - execArgs.Contains(Execution::Args::Type::Name) || - execArgs.Contains(Execution::Args::Type::Moniker) || - execArgs.Contains(Execution::Args::Type::Version) || - execArgs.Contains(Execution::Args::Type::Channel) || - execArgs.Contains(Execution::Args::Type::Source) || - execArgs.Contains(Execution::Args::Type::Exact))) + if (execArgs.Contains(Args::Type::Manifest) && + (execArgs.Contains(Args::Type::Query) || + execArgs.Contains(Args::Type::Id) || + execArgs.Contains(Args::Type::Name) || + execArgs.Contains(Args::Type::Moniker) || + execArgs.Contains(Args::Type::Version) || + execArgs.Contains(Args::Type::Channel) || + execArgs.Contains(Args::Type::Source) || + execArgs.Contains(Args::Type::Exact))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided); + } + + if (execArgs.Contains(Args::Type::InstallScope)) { - throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + if (ConvertToScopeEnum(execArgs.GetArg(Args::Type::InstallScope)) == Manifest::ScopeEnum::Unknown) + { + throw CommandException(Resource::String::InvalidArgumentValueError, s_ArgumentName_Scope, { "user"_lis, "machine"_lis }); + } } } - void InstallCommand::ExecuteInternal(Execution::Context& context) const + void InstallCommand::ExecuteInternal(Context& context) const { context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -37,6 +37,7 @@ namespace AppInstaller::CLI::Execution Log, Override, //Override args are (and the only args) directly passed to installer InstallLocation, + InstallScope, HashOverride, // Ignore hash mismatches //Source Command diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -50,6 +50,12 @@ namespace AppInstaller::CLI::Execution Max }; + struct PackagesToInstall + { + std::shared_ptr<Repository::IPackageVersion> PackageVersion; + PackageCollection::Package PackageRequest; + }; + namespace details { template <Data D> @@ -163,7 +169,7 @@ namespace AppInstaller::CLI::Execution template <> struct DataMapping<Data::PackagesToInstall> { - using value_t = std::vector<std::shared_ptr<Repository::IPackageVersion>>; + using value_t = std::vector<PackagesToInstall>; }; template <> diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp @@ -12,7 +12,7 @@ #include <algorithm> #include <ostream> -using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository; namespace AppInstaller::CLI { @@ -20,22 +20,32 @@ namespace AppInstaller::CLI { // Strings used in the Packages JSON file. // Most will be used to access a JSON value, so they need to be std::string - const std::string s_PackagesJson_Schema = "$schema"; - const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; - const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; - const std::string s_PackagesJson_CreationDate = "CreationDate"; - - const std::string s_PackagesJson_Sources = "Sources"; - const std::string s_PackagesJson_Source_Details = "SourceDetails"; - const std::string s_PackagesJson_Source_Name = "Name"; - const std::string s_PackagesJson_Source_Identifier = "Identifier"; - const std::string s_PackagesJson_Source_Argument = "Argument"; - const std::string s_PackagesJson_Source_Type = "Type"; - - const std::string s_PackagesJson_Packages = "Packages"; - const std::string s_PackagesJson_Package_Id = "Id"; - const std::string s_PackagesJson_Package_Version = "Version"; - const std::string s_PackagesJson_Package_Channel = "Channel"; + struct StaticStrings + { + const std::string PackagesJson_Schema = "$schema"; + const std::string PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; + const std::string PackagesJson_WinGetVersion = "WinGetVersion"; + const std::string PackagesJson_CreationDate = "CreationDate"; + + const std::string PackagesJson_Sources = "Sources"; + const std::string PackagesJson_Source_Details = "SourceDetails"; + const std::string PackagesJson_Source_Name = "Name"; + const std::string PackagesJson_Source_Identifier = "Identifier"; + const std::string PackagesJson_Source_Argument = "Argument"; + const std::string PackagesJson_Source_Type = "Type"; + + const std::string PackagesJson_Packages = "Packages"; + const std::string PackagesJson_Package_Id = "Id"; + const std::string PackagesJson_Package_Version = "Version"; + const std::string PackagesJson_Package_Channel = "Channel"; + const std::string PackagesJson_Package_Scope = "Scope"; + + static const StaticStrings& Instance() + { + static StaticStrings instance; + return instance; + } + }; // Gets or creates a property of a JSON object by its name. Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName, Json::ValueType valueType) @@ -55,11 +65,15 @@ namespace AppInstaller::CLI // Reads the description of a package from a Package node in the JSON. PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) { - std::string id = packageNode[s_PackagesJson_Package_Id].asString(); - std::string version = packageNode.isMember(s_PackagesJson_Package_Version) ? packageNode[s_PackagesJson_Package_Version].asString() : ""; - std::string channel = packageNode.isMember(s_PackagesJson_Package_Channel) ? packageNode[s_PackagesJson_Package_Channel].asString() : ""; + const auto& ss = StaticStrings::Instance(); + + std::string id = packageNode[ss.PackagesJson_Package_Id].asString(); + std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : ""; + std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : ""; + std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : ""; PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + package.Scope = Manifest::ConvertToScopeEnum(scope); return package; } @@ -67,15 +81,17 @@ namespace AppInstaller::CLI // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode) { + const auto& ss = StaticStrings::Instance(); + SourceDetails sourceDetails; - auto& detailsNode = sourceNode[s_PackagesJson_Source_Details]; - sourceDetails.Identifier = Utility::LocIndString{ detailsNode[s_PackagesJson_Source_Identifier].asString() }; - sourceDetails.Name = detailsNode[s_PackagesJson_Source_Name].asString(); - sourceDetails.Arg = detailsNode[s_PackagesJson_Source_Argument].asString(); - sourceDetails.Type = detailsNode[s_PackagesJson_Source_Type].asString(); + auto& detailsNode = sourceNode[ss.PackagesJson_Source_Details]; + sourceDetails.Identifier = Utility::LocIndString{ detailsNode[ss.PackagesJson_Source_Identifier].asString() }; + sourceDetails.Name = detailsNode[ss.PackagesJson_Source_Name].asString(); + sourceDetails.Arg = detailsNode[ss.PackagesJson_Source_Argument].asString(); + sourceDetails.Type = detailsNode[ss.PackagesJson_Source_Type].asString(); PackageCollection::Source source{ std::move(sourceDetails) }; - for (const auto& packageNode : sourceNode[s_PackagesJson_Packages]) + for (const auto& packageNode : sourceNode[ss.PackagesJson_Packages]) { source.Packages.emplace_back(ParsePackageNode(packageNode)); } @@ -86,14 +102,16 @@ namespace AppInstaller::CLI // Creates a minimal root object of a Packages JSON file. Json::Value CreateRoot(const std::string& wingetVersion) { + const auto& ss = StaticStrings::Instance(); + Json::Value root{ Json::ValueType::objectValue }; - root[s_PackagesJson_WinGetVersion] = wingetVersion; - root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_v1_0; + root[ss.PackagesJson_WinGetVersion] = wingetVersion; + root[ss.PackagesJson_Schema] = ss.PackagesJson_SchemaUri_v1_0; // TODO: This uses localtime. Do we want to use UTC or add time zone? std::stringstream currentTimeStream; Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now()); - root[s_PackagesJson_CreationDate] = currentTimeStream.str(); + root[ss.PackagesJson_CreationDate] = currentTimeStream.str(); return root; } @@ -101,41 +119,50 @@ namespace AppInstaller::CLI // Adds a new Package node to a Source node in the Json file, and returns it. Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageCollection::Package& package) { + const auto& ss = StaticStrings::Instance(); + Json::Value packageNode{ Json::ValueType::objectValue }; - packageNode[s_PackagesJson_Package_Id] = package.Id.get(); + packageNode[ss.PackagesJson_Package_Id] = package.Id.get(); // Only add version and channel if present. // Packages may not have a channel, or versions may not have been requested. const std::string& version = package.VersionAndChannel.GetVersion().ToString(); if (!version.empty()) { - packageNode[s_PackagesJson_Package_Version] = version; + packageNode[ss.PackagesJson_Package_Version] = version; } const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); if (!channel.empty()) { - packageNode[s_PackagesJson_Package_Channel] = channel; + packageNode[ss.PackagesJson_Package_Channel] = channel; + } + + if (package.Scope != Manifest::ScopeEnum::Unknown) + { + packageNode[ss.PackagesJson_Package_Scope] = std::string{ Manifest::ScopeToString(package.Scope) }; } - return sourceNode[s_PackagesJson_Packages].append(std::move(packageNode)); + return sourceNode[ss.PackagesJson_Packages].append(std::move(packageNode)); } // Adds a new Source node to the JSON, and returns it. Json::Value& AddSourceNode(Json::Value& root, const PackageCollection::Source& source) { + const auto& ss = StaticStrings::Instance(); + Json::Value sourceNode{ Json::ValueType::objectValue }; Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; - sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; - sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; - sourceDetailsNode[s_PackagesJson_Source_Identifier] = source.Details.Identifier; - sourceDetailsNode[s_PackagesJson_Source_Type] = source.Details.Type; - sourceNode[s_PackagesJson_Source_Details] = std::move(sourceDetailsNode); + sourceDetailsNode[ss.PackagesJson_Source_Name] = source.Details.Name; + sourceDetailsNode[ss.PackagesJson_Source_Argument] = source.Details.Arg; + sourceDetailsNode[ss.PackagesJson_Source_Identifier] = source.Details.Identifier; + sourceDetailsNode[ss.PackagesJson_Source_Type] = source.Details.Type; + sourceNode[ss.PackagesJson_Source_Details] = std::move(sourceDetailsNode); - sourceNode[s_PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue }; + sourceNode[ss.PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue }; - auto& sourcesNode = GetJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); + auto& sourcesNode = GetJsonProperty(root, ss.PackagesJson_Sources, Json::ValueType::arrayValue); for (const auto& package : source.Packages) { AddPackageToSource(sourceNode, package); @@ -160,16 +187,18 @@ namespace AppInstaller::CLI ParseResult TryParseJson(const Json::Value& root) { + const auto& ss = StaticStrings::Instance(); + // Find the schema used for the JSON - if (!(root.isObject() && root.isMember(s_PackagesJson_Schema) && root[s_PackagesJson_Schema].isString())) + if (!(root.isObject() && root.isMember(ss.PackagesJson_Schema) && root[ss.PackagesJson_Schema].isString())) { - AICLI_LOG(CLI, Error, << "Import file is missing \"" << s_PackagesJson_Schema << "\" property"); + AICLI_LOG(CLI, Error, << "Import file is missing \"" << ss.PackagesJson_Schema << "\" property"); return ParseResult{ ParseResult::Type::MissingSchema }; } - const auto& schemaUri = root[s_PackagesJson_Schema].asString(); + const auto& schemaUri = root[ss.PackagesJson_Schema].asString(); Json::Value schemaJson; - if (schemaUri == s_PackagesJson_SchemaUri_v1_0) + if (schemaUri == ss.PackagesJson_SchemaUri_v1_0) { schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V1), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE)); } @@ -191,8 +220,8 @@ namespace AppInstaller::CLI // Extract the data from the JSON. PackageCollection packages; - packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); - for (const auto& sourceNode : root[s_PackagesJson_Sources]) + packages.ClientVersion = root[ss.PackagesJson_WinGetVersion].asString(); + for (const auto& sourceNode : root[ss.PackagesJson_Sources]) { auto newSource = ParseSourceNode(sourceNode); auto existingSource = std::find_if(packages.Sources.begin(), packages.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; }); diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once - #include "AppInstallerDateTime.h" #include "AppInstallerRepositorySource.h" +#include <winget/Manifest.h> #include <json.h> @@ -28,6 +28,7 @@ namespace AppInstaller::CLI Utility::LocIndString Id; Utility::VersionAndChannel VersionAndChannel; + Manifest::ScopeEnum Scope = Manifest::ScopeEnum::Unknown; }; // A source along with a set of packages available from it. @@ -58,11 +59,11 @@ namespace AppInstaller::CLI UnrecognizedSchema, SchemaValidationFailed, Success, - }; + }; ParseResult(Type result) : Result(result) {} ParseResult(Type result, std::string_view errors) : Result(result), Errors(errors) {} - ParseResult(PackageCollection&& packages) : Result(Type::Success), Packages(std::move(packages)) {} + ParseResult(PackageCollection&& packages) : Result(Type::Success), Packages(std::move(packages)) {} Type Result; PackageCollection Packages; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -95,9 +95,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowStartingPackageInstall); WINGET_DEFINE_RESOURCE_STRINGID(InstallForceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstallScopeDescription); WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InvalidAliasError); WINGET_DEFINE_RESOURCE_STRINGID(InvalidArgumentSpecifierError); + WINGET_DEFINE_RESOURCE_STRINGID(InvalidArgumentValueError); WINGET_DEFINE_RESOURCE_STRINGID(InvalidJsonFile); WINGET_DEFINE_RESOURCE_STRINGID(InvalidNameError); WINGET_DEFINE_RESOURCE_STRINGID(LanguageArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -239,7 +239,7 @@ namespace AppInstaller::CLI::Workflow void SearchPackagesForImport(Execution::Context& context) { const auto& sources = context.Get<Execution::Data::Sources>(); - std::vector<std::shared_ptr<IPackageVersion>> packagesToInstall = {}; + std::vector<Execution::PackagesToInstall> packagesToInstall = {}; bool foundAll = true; // Look for the packages needed from each source independently. @@ -301,7 +301,7 @@ namespace AppInstaller::CLI::Workflow } } - packagesToInstall.push_back(std::move(searchContext.Get<Execution::Data::PackageVersion>())); + packagesToInstall.push_back({ std::move(searchContext.Get<Execution::Data::PackageVersion>()), packageRequest }); } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -416,8 +416,11 @@ namespace AppInstaller::CLI::Workflow Execution::Context& installContext = *installContextPtr; // Extract the data needed for installing - installContext.Add<Execution::Data::PackageVersion>(package); - installContext.Add<Execution::Data::Manifest>(package->GetManifest()); + installContext.Add<Execution::Data::PackageVersion>(package.PackageVersion); + installContext.Add<Execution::Data::Manifest>(package.PackageVersion->GetManifest()); + + // TODO: In the future, it would be better to not have to convert back and forth from a string + installContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(package.PackageRequest.Scope)); installContext << InstallPackageVersion; if (installContext.IsTerminated()) diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -3,93 +3,244 @@ #include "pch.h" #include "WorkflowBase.h" #include "ManifestComparator.h" +#include <winget/UserSettings.h> using namespace AppInstaller::CLI; using 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.InstallerType) << ',' << + AppInstaller::Manifest::ScopeToString(installer.Scope) << ',' << + installer.Locale << ']'; +} + namespace AppInstaller::CLI::Workflow { namespace { - // Determine if the installer is applicable. - // TODO: Implement a mechanism for better error messaging for no applicable installer scenario - bool IsInstallerApplicable(const Manifest::ManifestInstaller& installer, Manifest::InstallerTypeEnum installedType) + struct OSVersionFilter : public details::FilterField { - // Check MinOSVersion - if (!installer.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion))) + OSVersionFilter() : details::FilterField("OS Version") {} + + bool IsApplicable(const Manifest::ManifestInstaller& installer) override { - return false; + return installer.MinOSVersion.empty() || Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion)); } - if (Utility::IsApplicableArchitecture(installer.Arch) == Utility::InapplicableArchitecture) + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override { - return false; + std::string result = "Current OS is lower than MinOSVersion "; + result += installer.MinOSVersion; + return result; } + }; + + struct MachineArchitectureComparator : public details::ComparisonField + { + MachineArchitectureComparator() : details::ComparisonField("Machine Architecture") {} - if (installedType != Manifest::InstallerTypeEnum::Unknown && - !Manifest::IsInstallerTypeCompatible(installer.InstallerType, installedType)) + bool IsApplicable(const Manifest::ManifestInstaller& installer) override { + return Utility::IsApplicableArchitecture(installer.Arch) != Utility::InapplicableArchitecture; + } + + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override + { + std::string result = "Machine is not compatible with "; + result += Utility::ToString(installer.Arch); + return result; + } + + bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override + { + auto arch1 = Utility::IsApplicableArchitecture(first.Arch); + auto arch2 = Utility::IsApplicableArchitecture(second.Arch); + + if (arch1 > arch2) + { + return true; + } + return false; } + }; - return true; - } + struct InstalledTypeComparator : public details::ComparisonField + { + InstalledTypeComparator(Manifest::InstallerTypeEnum installedType) : + details::ComparisonField("Installed Type"), m_installedType(installedType) {} - // This is used in sorting the list of available installers to get the best match. - // Determines if installer1 is a better match than installer2. - bool IsInstallerBetterMatch( - const Manifest::ManifestInstaller& installer1, - const Manifest::ManifestInstaller& installer2, - Manifest::InstallerTypeEnum installedType) + static std::unique_ptr<InstalledTypeComparator> 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<InstalledTypeComparator>(installedType); + } + } + + return {}; + } + + bool IsApplicable(const Manifest::ManifestInstaller& installer) override + { + return Manifest::IsInstallerTypeCompatible(installer.InstallerType, m_installedType); + } + + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override + { + std::string result = "Installed package type is not compatible with "; + result += Manifest::InstallerTypeToString(installer.InstallerType); + return result; + } + + bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override + { + return (first.InstallerType == m_installedType && second.InstallerType != m_installedType); + } + + private: + Manifest::InstallerTypeEnum m_installedType; + }; + + struct InstalledScopeFilter : public details::FilterField { - // If there's installation metadata, pick the preferred one or compatible one - if (installedType != Manifest::InstallerTypeEnum::Unknown) + InstalledScopeFilter(Manifest::ScopeEnum requirement) : + details::FilterField("Installed Scope"), m_requirement(requirement) {} + + static std::unique_ptr<InstalledScopeFilter> Create(const Repository::IPackageVersion::Metadata& installationMetadata) { - if (installer1.InstallerType == installedType && installer2.InstallerType != installedType) + // Check for an existing install and require a matching scope. + auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); + if (installerScopeItr != installationMetadata.end()) { - return true; + Manifest::ScopeEnum installedScope = Manifest::ConvertToScopeEnum(installerScopeItr->second); + if (installedScope != Manifest::ScopeEnum::Unknown) + { + return std::make_unique<InstalledScopeFilter>(installedScope); + } } + + return {}; } - // Todo: Compare only architecture for now. Need more work and spec. - auto arch1 = Utility::IsApplicableArchitecture(installer1.Arch); - auto arch2 = Utility::IsApplicableArchitecture(installer2.Arch); + bool IsApplicable(const Manifest::ManifestInstaller& installer) override + { + // We have to assume the an unknown scope will match our required scope, or the entire catalog would stop working for upgrade. + return installer.Scope == Manifest::ScopeEnum::Unknown || installer.Scope == m_requirement; + } - if (arch1 > arch2) + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override { - return true; + std::string result = "Installer scope does not matched currently installed scope: "; + result += Manifest::ScopeToString(installer.Scope); + result += " != "; + result += Manifest::ScopeToString(m_requirement); + return result; } - return false; - } + private: + Manifest::ScopeEnum m_requirement; + }; + + struct ScopeComparator : public details::ComparisonField + { + ScopeComparator(Manifest::ScopeEnum preference, Manifest::ScopeEnum requirement) : + details::ComparisonField("Scope"), m_preference(preference), m_requirement(requirement) {} + + static std::unique_ptr<ScopeComparator> Create(const Execution::Args& args) + { + // Preference will always come from settings + Manifest::ScopeEnum preference = ConvertScope(Settings::User().Get<Settings::Setting::InstallScopePreference>()); + + // Requirement may come from args or settings; args overrides settings. + Manifest::ScopeEnum requirement = Manifest::ScopeEnum::Unknown; + + if (args.Contains(Execution::Args::Type::InstallScope)) + { + requirement = Manifest::ConvertToScopeEnum(args.GetArg(Execution::Args::Type::InstallScope)); + } + else + { + requirement = ConvertScope(Settings::User().Get<Settings::Setting::InstallScopeRequirement>()); + } + + if (preference != Manifest::ScopeEnum::Unknown || requirement != Manifest::ScopeEnum::Unknown) + { + return std::make_unique<ScopeComparator>(preference, requirement); + } + else + { + return {}; + } + } + + bool IsApplicable(const Manifest::ManifestInstaller& installer) override + { + return m_requirement == Manifest::ScopeEnum::Unknown || installer.Scope == m_requirement; + } + + 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; + } + + bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override + { + return m_preference != Manifest::ScopeEnum::Unknown && (first.Scope == m_preference && second.Scope != m_preference); + } + + private: + static Manifest::ScopeEnum ConvertScope(Settings::ScopePreference scope) + { + switch (scope) + { + case Settings::ScopePreference::None: return Manifest::ScopeEnum::Unknown; + case Settings::ScopePreference::User: return Manifest::ScopeEnum::User; + case Settings::ScopePreference::Machine: return Manifest::ScopeEnum::Machine; + } + + return Manifest::ScopeEnum::Unknown; + } + + Manifest::ScopeEnum m_preference; + Manifest::ScopeEnum m_requirement; + }; + } + + ManifestComparator::ManifestComparator(const Execution::Args& args, const Repository::IPackageVersion::Metadata& installationMetadata) + { + AddFilter(std::make_unique<OSVersionFilter>()); + AddFilter(InstalledScopeFilter::Create(installationMetadata)); + + // Filter order is not important, but comparison order determines priority. + // TODO: There are improvements to be made here around ordering, especially in the context of implicit vs explicit vs command line preferences. + AddComparator(InstalledTypeComparator::Create(installationMetadata)); + AddComparator(ScopeComparator::Create(args)); + AddComparator(std::make_unique<MachineArchitectureComparator>()); } std::optional<Manifest::ManifestInstaller> ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) { AICLI_LOG(CLI, Info, << "Starting installer selection."); - // Get the currently installed package's type (if present) - Manifest::InstallerTypeEnum installedType = Manifest::InstallerTypeEnum::Unknown; - auto installerTypeItr = m_installationMetadata.find(Repository::PackageVersionMetadata::InstalledType); - if (installerTypeItr != m_installationMetadata.end()) - { - installedType = Manifest::ConvertToInstallerTypeEnum(installerTypeItr->second); - } - const Manifest::ManifestInstaller* result = nullptr; for (const auto& installer : manifest.Installers) { - if (!result) - { - if (IsInstallerApplicable(installer, installedType)) - { - result = &installer; - } - } - else if (IsInstallerApplicable(installer, installedType) && IsInstallerBetterMatch(installer, *result, installedType)) + if (IsApplicable(installer) && (!result || IsFirstBetter(installer, *result))) { + AICLI_LOG(CLI, Verbose, << "Installer " << installer << " is current best choice"); result = &installer; } } @@ -108,4 +259,60 @@ namespace AppInstaller::CLI::Workflow return *result; } + + // TODO: Implement a mechanism for better error messaging for no applicable installer scenario + bool ManifestComparator::IsApplicable(const Manifest::ManifestInstaller& installer) + { + for (const auto& filter : m_filters) + { + if (!filter->IsApplicable(installer)) + { + AICLI_LOG(CLI, Info, << "Installer " << installer << " not applicable: " << filter->ExplainInapplicable(installer)); + return false; + } + } + + return true; + } + + bool ManifestComparator::IsFirstBetter( + const Manifest::ManifestInstaller& first, + const Manifest::ManifestInstaller& second) + { + for (auto comparator : m_comparators) + { + if (comparator->IsFirstBetter(first, second)) + { + AICLI_LOG(CLI, Verbose, << "Installer " << first << " is better than " << second << " due to: " << comparator->Name()); + return true; + } + else if (comparator->IsFirstBetter(second, first)) + { + // Second is better by this comparator, don't allow a lower priority one to override that. + AICLI_LOG(CLI, Verbose, << "Installer " << second << " is better than " << first << " due to: " << comparator->Name()); + return false; + } + } + + // 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)); + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -5,20 +5,71 @@ #include <winget/Manifest.h> #include <AppInstallerRepositorySearch.h> +#include <memory> +#include <string> +#include <string_view> +#include <vector> + namespace AppInstaller::CLI::Workflow { + 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 bool 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; + }; + + // 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 bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) = 0; + }; + } + // Class in charge of comparing manifest entries struct ManifestComparator { - ManifestComparator(const Execution::Args&, Repository::IPackageVersion::Metadata installationMetadata = {}) : - m_installationMetadata(std::move(installationMetadata)) {} + ManifestComparator(const Execution::Args&, const Repository::IPackageVersion::Metadata& installationMetadata); + // Gets the best installer from the manifest, if at least one is applicable. std::optional<Manifest::ManifestInstaller> GetPreferredInstaller(const Manifest::Manifest& manifest); + // Determines if an installer is applicable. + bool 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: - // TODO: Handle args to change how we select. - Repository::IPackageVersion::Metadata m_installationMetadata; + 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/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -620,7 +620,7 @@ namespace AppInstaller::CLI::Workflow installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); } - ManifestComparator manifestComparator(context.Args, std::move(installationMetadata)); + ManifestComparator manifestComparator(context.Args, installationMetadata); context.Add<Execution::Data::Installer>(manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>())); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -386,7 +386,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Filter results by name</value> </data> <data name="NoApplicableInstallers" xml:space="preserve"> - <value>No installers are applicable to the current system.</value> + <value>No applicable installer found; see logs for more details.</value> </data> <data name="NoExperimentalFeaturesMessage" xml:space="preserve"> <value>There are currently no experimental features available. </value> @@ -817,6 +817,14 @@ They can be configured through the settings file 'winget settings'.</value> <data name="ImportFileHasInvalidSchema" xml:space="preserve"> <value>The JSON file does not specify a recognized schema.</value> </data> + <data name="InstallScopeDescription" xml:space="preserve"> + <value>Select install scope (user or machine)</value> + <comment>This argument allows the user to select between installing for just the user or for the entire machine.</comment> + </data> + <data name="InvalidArgumentValueError" xml:space="preserve"> + <value>The value provided for the `%1` argument is invalid; valid values are</value> + <comment>{Locked="%1"} The value will be replaced with the argument name</comment> + </data> <data name="DisabledByGroupPolicy" xml:space="preserve"> <value>This operation is disabled by Group Policy</value> </data> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -189,6 +189,7 @@ <ClCompile Include="ExperimentalFeature.cpp" /> <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="HashCommand.cpp" /> + <ClCompile Include="ManifestComparator.cpp" /> <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="PackageCollection.cpp" /> @@ -265,6 +266,9 @@ <CopyFileToFolders Include="TestData\ImportFile-Good-AlreadyInstalled.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-MachineScope.json"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <None Include="packages.config" /> <None Include="PropertySheet.props" /> <CopyFileToFolders Include="TestData\InstallerArgTest_Inno_NoSwitches.yaml"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -137,6 +137,9 @@ <ClCompile Include="TestSettings.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="ManifestComparator.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -411,5 +414,11 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe_2.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-MachineScope.json"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -141,7 +141,8 @@ struct CommandExceptionMatcher : public Catch::MatcherBase<CommandException> bool match(const CommandException& ce) const override { - return ce.Param().get() == m_expectedArg; + const auto& params = ce.Params(); + return params.size() == 1 && params[0].get() == m_expectedArg; } std::string describe() const override @@ -159,7 +160,32 @@ namespace Catch { template<> struct StringMaker<CommandException> { static std::string convert(CommandException const& ce) { - return std::string{ "CommandException{ '" } + ce.Message().get() + "', '" + ce.Param().get() + "'}"; + std::string result{ "CommandException{ '" }; + result += ce.Message().get(); + result += '\''; + + bool first = true; + for (const auto& param : ce.Params()) + { + if (first) + { + first = false; + result += ", ['"; + } + else + { + result += "', '"; + } + result += param.get(); + } + + if (!first) + { + result += "']"; + } + + result += " }"; + return result; } }; } diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <Workflows/ManifestComparator.h> +#include <winget/UserSettings.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller::CLI::Workflow; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Settings; +using namespace AppInstaller::Utility; + +using Manifest = ::AppInstaller::Manifest::Manifest; + +const ManifestInstaller& AddInstaller(Manifest& manifest, Architecture architecture, InstallerTypeEnum installerType, ScopeEnum scope = ScopeEnum::Unknown, std::string minOSVersion = {}) +{ + ManifestInstaller toAdd; + toAdd.Arch = architecture; + toAdd.InstallerType = installerType; + toAdd.Scope = scope; + toAdd.MinOSVersion = minOSVersion; + + manifest.Installers.emplace_back(std::move(toAdd)); + + return manifest.Installers.back(); +} + +void RequireInstaller(const std::optional<ManifestInstaller>& actual, const ManifestInstaller& expected) +{ + REQUIRE(actual); + REQUIRE(actual->Arch == expected.Arch); + REQUIRE(actual->InstallerType == expected.InstallerType); + REQUIRE(actual->Scope == expected.Scope); + REQUIRE(actual->MinOSVersion == expected.MinOSVersion); +} + +TEST_CASE("ManifestComparator_OSFilter_Low", "[manifest_comparator]") +{ + Manifest manifest; + AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::Unknown, "10.0.99999.0"); + + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + REQUIRE(!result); +} + +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({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, expected); +} + +TEST_CASE("ManifestComparator_InstalledScopeFilter_Uknown", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller unknown = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::Unknown); + + SECTION("Nothing Installed") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // Only because it is first + RequireInstaller(result, unknown); + } + SECTION("User Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::User); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, unknown); + } + SECTION("Machine Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::Machine); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, unknown); + } +} + +TEST_CASE("ManifestComparator_InstalledScopeFilter", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller user = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::User); + ManifestInstaller machine = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::Machine); + + SECTION("Nothing Installed") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // Only because it is first + RequireInstaller(result, user); + } + SECTION("User Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::User); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, user); + } + SECTION("Machine Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledScope] = ScopeToString(ScopeEnum::Machine); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, machine); + } +} + +TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller msi = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi); + ManifestInstaller msix = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msix); + + SECTION("Nothing Installed") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // Only because it is first + RequireInstaller(result, msi); + } + SECTION("MSI Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Msi); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msi); + } + SECTION("MSIX Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Msix); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + } +} + +TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller burn = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Burn); + ManifestInstaller exe = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Exe); + + SECTION("Nothing Installed") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // Only because it is first + RequireInstaller(result, burn); + } + SECTION("Exe Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Exe); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + } + SECTION("Inno Installed") + { + IPackageVersion::Metadata metadata; + metadata[PackageVersionMetadata::InstalledType] = InstallerTypeToString(InstallerTypeEnum::Inno); + + ManifestComparator mc({}, metadata); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, burn); + } +} + +TEST_CASE("ManifestComparator_ScopeFilter", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller user = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::User); + ManifestInstaller machine = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::Machine); + + SECTION("Nothing Required") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // Only because it is first + RequireInstaller(result, user); + } + SECTION("User Required") + { + Args args; + args.AddArg(Args::Type::InstallScope, ScopeToString(ScopeEnum::User)); + + ManifestComparator mc(args, {}); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, user); + } + SECTION("Machine Required") + { + Args args; + args.AddArg(Args::Type::InstallScope, ScopeToString(ScopeEnum::Machine)); + + ManifestComparator mc(args, {}); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, machine); + } +} + +TEST_CASE("ManifestComparator_ScopeCompare", "[manifest_comparator]") +{ + Manifest manifest; + ManifestInstaller machine = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::Machine); + ManifestInstaller user = AddInstaller(manifest, Architecture::Neutral, InstallerTypeEnum::Msi, ScopeEnum::User); + + SECTION("No Preference") + { + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + // The default preference is user + RequireInstaller(result, user); + } + SECTION("User Preference") + { + TestUserSettings settings; + settings.Set<Setting::InstallScopePreference>(ScopePreference::User); + + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, user); + } + SECTION("Machine Preference") + { + TestUserSettings settings; + settings.Set<Setting::InstallScopePreference>(ScopePreference::Machine); + + ManifestComparator mc({}, {}); + auto result = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, machine); + } +} diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestHooks.h" #include "winget/GroupPolicy.h" #include "winget/UserSettings.h" @@ -198,4 +199,19 @@ namespace TestCommon { THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(DWORD))); } + + TestUserSettings::TestUserSettings(bool keepFileSettings) + { + if (!keepFileSettings) + { + m_settings.clear(); + } + + AppInstaller::Settings::SetUserSettingsOverride(this); + } + + TestUserSettings::~TestUserSettings() + { + AppInstaller::Settings::SetUserSettingsOverride(nullptr); + } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerLogging.h> #include <AppInstallerProgress.h> +#include <winget/UserSettings.h> #include <wil/result.h> #include <filesystem> @@ -114,4 +115,19 @@ namespace TestCommon void SetRegistryValue(HKEY key, const std::wstring& name, const std::wstring& value, DWORD type = REG_SZ); void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector<BYTE>& value, DWORD type = REG_BINARY); void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value); + + // Override UserSettings using this class. + // Automatically overrides the user settings for the lifetime of this object. + // DOES NOT SUPPORT NESTED USE + struct TestUserSettings : public AppInstaller::Settings::UserSettings + { + TestUserSettings(bool keepFileSettings = false); + ~TestUserSettings(); + + template <AppInstaller::Settings::Setting S> + void Set(typename AppInstaller::Settings::details::SettingMapping<S>::value_t&& value) + { + m_settings[S].emplace<AppInstaller::Settings::details::SettingIndex(S)>(std::move(value)); + } + }; } diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good-MachineScope.json b/src/AppInstallerCLITests/TestData/ImportFile-Good-MachineScope.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "TestExeInstallerWithNothingInstalled", + "Version": "1.0.0.0", + "Scope": "machine" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml @@ -4,14 +4,25 @@ Name: AppInstaller Test Exe Installer Publisher: Microsoft Corporation AppMoniker: AICLITestExe License: Test -Switches: - Custom: /custom - SilentWithProgress: /silentwithprogress - Silent: /silence - Update: /update Installers: - Arch: x64 Url: https://ThisIsNotUsed InstallerType: exe Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + Scope: user + Switches: + Custom: /custom /scope=user + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: exe + Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + Scope: machine + Switches: + Custom: /custom /scope=machine + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -9,6 +9,7 @@ #include <AppInstallerTelemetry.h> #include <AppInstallerRuntime.h> +#include <winget/UserSettings.h> #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -32,4 +33,9 @@ namespace AppInstaller { void TestHook_SetTelemetryOverride(std::shared_ptr<TelemetryTraceLogger> ttl); } + + namespace Settings + { + void SetUserSettingsOverride(UserSettings* value); + } } diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -188,6 +188,18 @@ namespace PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } + if (input == "TestExeInstallerWithNothingInstalled") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + std::vector<Manifest>{ manifest }, + this->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); + } + return result; } }; @@ -1346,6 +1358,29 @@ TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } +TEST_CASE("ImportFlow_MachineScope", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-MachineScope.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify all packages were installed + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); + std::ifstream installResultFile(exeInstallResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/scope=machine") != std::string::npos); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -321,11 +321,11 @@ namespace AppInstaller::Logging } AICLI_LOG(CLI, Info, << "Completed installer selection."); - AICLI_LOG(CLI, Verbose, << "Selected installer arch: " << arch); - AICLI_LOG(CLI, Verbose, << "Selected installer url: " << url); + AICLI_LOG(CLI, Verbose, << "Selected installer Architecture: " << arch); + AICLI_LOG(CLI, Verbose, << "Selected installer URL: " << url); AICLI_LOG(CLI, Verbose, << "Selected installer InstallerType: " << installerType); - AICLI_LOG(CLI, Verbose, << "Selected installer scope: " << scope); - AICLI_LOG(CLI, Verbose, << "Selected installer language: " << language); + AICLI_LOG(CLI, Verbose, << "Selected installer Scope: " << scope); + AICLI_LOG(CLI, Verbose, << "Selected installer Language: " << language); } void TelemetryTraceLogger::LogSearchRequest( diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -178,7 +178,7 @@ namespace AppInstaller::Manifest return result; } - ScopeEnum ConvertToScopeEnum(const std::string& in) + ScopeEnum ConvertToScopeEnum(std::string_view in) { ScopeEnum result = ScopeEnum::Unknown; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -44,7 +44,9 @@ namespace AppInstaller::Manifest return in1.Locale < in2.Locale; } - if (in1.Scope != in2.Scope) + // Unknown is considered equal to all other values for uniqueness. + // If either value is unknown, don't compare them. + if (in1.Scope != in2.Scope && in1.Scope != ScopeEnum::Unknown && in2.Scope != ScopeEnum::Unknown) { return in1.Scope < in2.Scope; } diff --git a/src/AppInstallerCommonCore/Public/winget/LocIndependent.h b/src/AppInstallerCommonCore/Public/winget/LocIndependent.h @@ -11,19 +11,10 @@ namespace AppInstaller::Utility // Used as a wrapper around strings that do not need localization. struct LocIndView : public std::string_view { + constexpr LocIndView() = default; explicit constexpr LocIndView(std::string_view sv) : std::string_view(sv) {} }; - namespace literals - { - // "I solemnly swear that this string is indeed localization independent." - // Enable easier use of a localization independent view through literals. - inline LocIndView operator ""_liv(const char* chars, size_t size) - { - return LocIndView{ std::string_view{ chars, size } }; - } - } - // "I solemnly swear that this string is indeed localization independent." // A localization independent string; either through external localization // or by virtue of not needing to be localized. @@ -61,4 +52,21 @@ namespace AppInstaller::Utility private: std::string m_value; }; + + namespace literals + { + // "I solemnly swear that this string is indeed localization independent." + // Enable easier use of a localization independent view through literals. + inline constexpr LocIndView operator ""_liv(const char* chars, size_t size) + { + return LocIndView{ std::string_view{ chars, size } }; + } + + // "I solemnly swear that this string is indeed localization independent." + // Enable easier use of a localization independent string through literals. + inline LocIndString operator ""_lis(const char* chars, size_t size) + { + return LocIndString{ std::string_view{ chars, size } }; + } + } } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -5,6 +5,7 @@ #include <AppInstallerVersions.h> #include <map> +#include <string_view> namespace AppInstaller::Manifest { @@ -130,7 +131,7 @@ namespace AppInstaller::Manifest UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); - ScopeEnum ConvertToScopeEnum(const std::string& in); + ScopeEnum ConvertToScopeEnum(std::string_view in); InstallModeEnum ConvertToInstallModeEnum(const std::string& in); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -20,7 +20,7 @@ namespace AppInstaller::Manifest { using string_t = Utility::NormalizedString; - AppInstaller::Utility::Architecture Arch; + AppInstaller::Utility::Architecture Arch = AppInstaller::Utility::Architecture::Unknown; string_t Url; @@ -42,7 +42,7 @@ namespace AppInstaller::Manifest // If present, has more precedence than root InstallerTypeEnum InstallerType = InstallerTypeEnum::Unknown; - ScopeEnum Scope = ScopeEnum::User; + ScopeEnum Scope = ScopeEnum::Unknown; std::vector<InstallModeEnum> InstallModes; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -38,6 +38,14 @@ namespace AppInstaller::Settings Rainbow, }; + // The preferred scope for installs. + enum class ScopePreference + { + None, + User, + Machine, + }; + // Enum of settings. // Must start at 0 to enable direct access to variant in UserSettings. // Max must be last and unused. @@ -59,6 +67,8 @@ namespace AppInstaller::Settings EFExport, TelemetryDisable, EFRestSource, + InstallScopePreference, + InstallScopeRequirement, Max }; @@ -109,6 +119,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExport, bool, bool, false, ".experimentalFeatures.export"sv); SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFRestSource, bool, bool, false, ".experimentalFeatures.restSource"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopePreference, std::string, ScopePreference, ScopePreference::User, ".installBehavior.preferences.scope"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, ScopePreference, ScopePreference::None, ".installBehavior.requirements.scope"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template <size_t... I> @@ -138,11 +150,7 @@ namespace AppInstaller::Settings bool IsFieldWarning = true; }; - static UserSettings const& Instance() - { - static UserSettings userSettings; - return userSettings; - } + static UserSettings const& Instance(); static std::filesystem::path SettingsFilePath(); @@ -170,15 +178,13 @@ namespace AppInstaller::Settings return std::get<details::SettingIndex(S)>(itr->second); } - private: + protected: UserSettingsType m_type = UserSettingsType::Default; std::vector<Warning> m_warnings; std::map<Setting, details::SettingVariant> m_settings; - protected: UserSettings(); ~UserSettings() = default; - }; inline UserSettings const& User() diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -178,22 +178,23 @@ namespace AppInstaller::Settings namespace details { +#define WINGET_VALIDATE_SIGNATURE(_setting_) \ + std::optional<SettingMapping<Setting::_setting_>::value_t> \ + SettingMapping<Setting::_setting_>::Validate(const SettingMapping<Setting::_setting_>::json_t& value) + // Stamps out a validate function that simply returns the input value. #define WINGET_VALIDATE_PASS_THROUGH(_setting_) \ - std::optional<SettingMapping<Setting::_setting_>::value_t> \ - SettingMapping<Setting::_setting_>::Validate(const SettingMapping<Setting::_setting_>::json_t& value) \ + WINGET_VALIDATE_SIGNATURE(_setting_) \ { \ return value; \ } - std::optional<SettingMapping<Setting::AutoUpdateTimeInMinutes>::value_t> - SettingMapping<Setting::AutoUpdateTimeInMinutes>::Validate(const SettingMapping<Setting::AutoUpdateTimeInMinutes>::json_t& value) + WINGET_VALIDATE_SIGNATURE(AutoUpdateTimeInMinutes) { return std::chrono::minutes(value); } - std::optional<SettingMapping<Setting::ProgressBarVisualStyle>::value_t> - SettingMapping<Setting::ProgressBarVisualStyle>::Validate(const SettingMapping<Setting::ProgressBarVisualStyle>::json_t& value) + WINGET_VALIDATE_SIGNATURE(ProgressBarVisualStyle) { // progressBar property possible values static constexpr std::string_view s_progressBar_Accent = "accent"; @@ -226,6 +227,51 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExport) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(EFRestSource) + + WINGET_VALIDATE_SIGNATURE(InstallScopePreference) + { + static constexpr std::string_view s_scope_user = "user"; + static constexpr std::string_view s_scope_machine = "machine"; + + if (Utility::CaseInsensitiveEquals(value, s_scope_user)) + { + return ScopePreference::User; + } + else if (Utility::CaseInsensitiveEquals(value, s_scope_machine)) + { + return ScopePreference::Machine; + } + + return {}; + } + + WINGET_VALIDATE_SIGNATURE(InstallScopeRequirement) + { + return SettingMapping<Setting::InstallScopePreference>::Validate(value); + } + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static UserSettings* s_UserSettings_Override = nullptr; + + void SetUserSettingsOverride(UserSettings* value) + { + s_UserSettings_Override = value; + } +#endif + + UserSettings const& UserSettings::Instance() + { + static UserSettings userSettings; + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_UserSettings_Override) + { + return *s_UserSettings_Override; + } +#endif + + return userSettings; } UserSettings::UserSettings() : m_type(UserSettingsType::Default)