diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 4f19d0b8bb..97f0c0e155 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -351,6 +351,7 @@ und UNICODESTRING uninstalling Unmarshal +unskipped untimes updatefile updatemanifest @@ -377,6 +378,7 @@ wcsicmp webpage WHOLECHAIN wil +windbg wincrypt WINEVENT winget diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f52370e92b..88a83c1e01 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -114,6 +114,7 @@ ECustom EFGH EFile efileresource +EMalicious endregion ENDSESSION EPester @@ -158,6 +159,7 @@ GRPICONDIR GRPICONDIRENTRY guiddef Hackathon +hashtables helplib helplibrary hhx @@ -214,6 +216,7 @@ JToken JValue Kaido KNOWNFOLDERID +kool ktf ldcase learnxinyminutes @@ -293,7 +296,7 @@ NESTEDINSTALLER netfx netlify NETSDK -Newtonsoft +Newtonsoft nlohmann NNS NOAGGREGATION diff --git a/.gitignore b/.gitignore index 705f174d84..af9e14a497 100644 --- a/.gitignore +++ b/.gitignore @@ -345,3 +345,6 @@ src/PowerShell/Microsoft.WinGet.Client/Crescendo/*.psm1 # Dev PowerShell module path src/PowerShell/scripts/Module + +# Interop nuget +src/WinGetUtilInterop/scripts/Nuget* diff --git a/.vsconfig b/.vsconfig index 101e664b78..453964198e 100644 --- a/.vsconfig +++ b/.vsconfig @@ -49,6 +49,7 @@ "Microsoft.VisualStudio.Component.VC.TestAdapterForBoostTest", "Microsoft.VisualStudio.Component.VC.TestAdapterForGoogleTest", "Microsoft.VisualStudio.Component.VC.ASAN", + "Microsoft.VisualStudio.Component.Vcpkg", "Microsoft.VisualStudio.Component.Windows10SDK.19041", "Microsoft.VisualStudio.Component.Windows11SDK.22000", "Microsoft.VisualStudio.Workload.NativeDesktop", diff --git a/README.md b/README.md index 390dc15ffb..33d044433a 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ If you are new to the Windows Package Manager, you might want to [Explore the Windows Package Manager tool](https://docs.microsoft.com/learn/modules/explore-windows-package-manager-tool/?WT.mc_id=AZ-MVP-5004737). The client has access to packages from two default sources. The first is "msstore" the Microsoft Store (free Apps rated "e" for everyone). The second is "winget" the [WinGet community repository](https://github.com/microsoft/winget-pkgs). -> **Note**: Group policy may be configured and modify configured sources. Run `winget --info` to see any configured policies. +> [!NOTE] +> Group policy may be configured and modify configured sources. Run `winget --info` to see any configured policies. ## Installing The Client -> **Note**: The client requires Windows 10 1809 (build 17763) or later at this time. Windows Server 2019 is not supported as the Microsoft Store is not available nor are updated dependencies. It may be possible to install on Windows Server 2022, this should be considered experimental (not supported), and requires dependencies to be manually installed as well. +> [!NOTE] +> The client requires Windows 10 1809 (build 17763) or later at this time. Windows Server 2019 is not supported as the Microsoft Store is not available nor are updated dependencies. It may be possible to install on Windows Server 2022, this should be considered experimental (not supported), and requires dependencies to be manually installed as well. ### Microsoft Store [Recommended] @@ -24,7 +26,8 @@ There are two methods to get development releases: * Install a [Windows 10 or Windows 11 Insider](https://insider.windows.com/) build. * Join the Windows Package Manager Insider program by [signing up](http://aka.ms/winget-InsiderProgram). -> **Note**: It may take a few days to get the updated App Installer after you receive e-mail confirmation from joining the Windows Package Manager Insider program. If you decide to install the latest release from GitHub, and you have successfully joined the insider program, you will receive updates when the next development release has been published in the Microsoft Store. +> [!NOTE] +> It may take a few days to get the updated App Installer after you receive e-mail confirmation from joining the Windows Package Manager Insider program. If you decide to install the latest release from GitHub, and you have successfully joined the insider program, you will receive updates when the next development release has been published in the Microsoft Store. Once you have received the updated App Installer from the Microsoft Store you should be able to execute `winget features` to see experimental features. Some users have reported [issues](https://github.com/microsoft/winget-cli/issues/210) with the client not being on their PATH. @@ -32,7 +35,8 @@ Once you have received the updated App Installer from the Microsoft Store you sh The same Microsoft Store package will be made available via our [Releases](https://github.com/microsoft/winget-cli/releases). Note that installing this package will give you the WinGet client, but it will not enable automatic updates from the Microsoft Store if you have not joined the Windows Package Manager Insider program. -> **Note**: You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). +> [!NOTE] +> You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). > This should only be necessary on older builds of Windows 10 and only if you get an error about missing framework packages. ### Troubleshooting @@ -84,7 +88,8 @@ The client is built around the concept of sources; a set of packages effectively 1. Clone the repository 2. Configure your system, please use the [configuration file](.configurations/configuration.dsc.yaml). This can be applied by either: * [Dev Home](https://github.com/microsoft/devhome)'s machine configuration tool - * WinGet configuration. If you have the experimental feature enabled, run `winget configure .configurations/configuration.dsc.yaml` from the project root so relative paths resolve correctly + * WinGet configuration. Run `winget configure .configurations/configuration.dsc.yaml` from the project root so relative paths resolve correctly +3. Run `vcpkg integrate install` from Developer Command Prompt for VS 2022. This is one time setup step until configuration file in step 2 is updated to work with vcpkg setup. ### Prerequisites @@ -96,8 +101,10 @@ The client is built around the concept of sources; a set of packages effectively * .NET Desktop Development * Desktop Development with C++ * Universal Windows Platform Development + * Check [.vsconfig file](.vsconfig) for full components list * [Windows SDK for Windows 11 (10.0.22000.194)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) - > **Note**: You can also get it through `winget install Microsoft.WindowsSDK --version 10.0.22000.832` (use --force if you have a newer version installed) or via Visual Studio > Get Tools and Features > Individual Components > Windows 10 SDK (10.0.22000.0) +> [!NOTE] +> You can also get it through `winget install Microsoft.WindowsSDK --version 10.0.22000.832` (use --force if you have a newer version installed) or via Visual Studio > Get Tools and Features > Individual Components > Windows 10 SDK (10.0.22000.0) * The following extensions: * [Microsoft Visual Studio Installer Projects](https://marketplace.visualstudio.com/items?itemName=VisualStudioClient.MicrosoftVisualStudio2022InstallerProjects) diff --git a/doc/Settings.md b/doc/Settings.md index f4dbe70bb0..f58469e0bc 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -332,11 +332,11 @@ Currently, this means that properly attributed configuration units (and only tho "experimentalFeatures": { "configureSelfElevate": true }, -``` - +``` + ### storeDownload -This feature enables packages to be downloaded from the Microsoft Store. +This feature enables packages to be downloaded from the Microsoft Store. You can enable the feature as shown below. ```json @@ -344,3 +344,14 @@ You can enable the feature as shown below. "storeDownload": true }, ``` + +### configureExport + +This feature enables exporting a configuration file. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configureExport": true + }, +``` diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md index 63a9dda0e6..d4ee7fff31 100644 --- a/doc/windows/package-manager/winget/returnCodes.md +++ b/doc/windows/package-manager/winget/returnCodes.md @@ -200,6 +200,9 @@ Installation failed. Restart your PC then try again. | | 0x8A15C00C | -1978286068 | WINGET_CONFIG_ERROR_SET_DEPENDENCY_CYCLE | The dependency graph contains a cycle which cannot be resolved. | | 0x8A15C00D | -1978286067 | WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE | The configuration has an invalid field value. | | 0x8A15C00E | -1978286066 | WINGET_CONFIG_ERROR_MISSING_FIELD | The configuration is missing a field. | +| 0x8A15C00F | -1978286065 | WINGET_CONFIG_ERROR_TEST_FAILED | Some of the configuration units failed while testing their state. | +| 0x8A15C010 | -1978286064 | WINGET_CONFIG_ERROR_TEST_NOT_RUN | Configuration state was not tested. | +| 0x8A15C011 | -1978286063 | WINGET_CONFIG_ERROR_GET_FAILED | The configuration unit failed getting its properties. | ## Configuration Processor Errors @@ -216,3 +219,4 @@ Installation failed. Restart your PC then try again. | | 0x8A15C109 | -1978285815 | WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT | The configuration unit returned an unexpected result during execution. | | 0x8A15C110 | -1978285814 | WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT | A unit contains a setting that requires the config root. | | 0x8A15C111 | -1978285813 | WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE_ADMIN | Loading the module for the configuration unit failed because it requires administrator privileges to run. | +| 0x8A15C112 | -1978285812 | WINGET_CONFIG_ERROR_NOT_SUPPORTED_BY_PROCESSOR | Operation is not supported by the configuration processor. | diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 0f800e9e73..8f8ce786da 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -290,6 +290,11 @@ "description": "Enable support for downloading packages from the Microsoft Store", "type": "boolean", "default": false + }, + "configureExport": { + "description": "Enable support for the configure export command", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 26adbfa273..d19b4af75e 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -384,6 +384,7 @@ + @@ -446,6 +447,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index dde62a5482..a6a7ab1e4b 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -251,6 +251,9 @@ Header Files + + Commands + @@ -472,6 +475,9 @@ Source Files + + Commands + diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 2d4b3132a9..0196d16a8c 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -141,8 +141,6 @@ namespace AppInstaller::CLI return { type, "position"_liv }; // Export Command - case Execution::Args::Type::OutputFile: - return { type, "output"_liv, 'o' }; case Execution::Args::Type::IncludeVersions: return { type, "include-versions"_liv }; @@ -211,6 +209,12 @@ namespace AppInstaller::CLI return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; case Execution::Args::Type::ConfigurationModulePath: return { type, "module-path"_liv }; + case Execution::Args::Type::ConfigurationExportPackageId: + return { type, "package-id"_liv }; + case Execution::Args::Type::ConfigurationExportModule: + return { type, "module"_liv }; + case Execution::Args::Type::ConfigurationExportResource: + return { type, "resource"_liv }; // Download command case Execution::Args::Type::DownloadDirectory: @@ -241,6 +245,8 @@ namespace AppInstaller::CLI return { type, "open-logs"_liv, "logs"_liv }; case Execution::Args::Type::Force: return { type, "force"_liv, ArgTypeCategory::CopyFlagToSubContext }; + case Execution::Args::Type::OutputFile: + return { type, "output"_liv, 'o' }; case Execution::Args::Type::DependencySource: return { type, "dependency-source"_liv, ArgTypeCategory::ExtendedSource }; diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h index 15804ab86f..d1260967b7 100644 --- a/src/AppInstallerCLICore/Command.h +++ b/src/AppInstallerCLICore/Command.h @@ -57,6 +57,8 @@ namespace AppInstaller::CLI Command(name, {}, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, Settings::TogglePolicy::Policy::None, outputFlags) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Command::Visibility visibility) : Command(name, aliases, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} + Command(std::string_view name, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : + Command(name, {}, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : Command(name, aliases, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp index 3b8f6daed6..93b83cbc8e 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -5,6 +5,7 @@ #include "ConfigureShowCommand.h" #include "ConfigureTestCommand.h" #include "ConfigureValidateCommand.h" +#include "ConfigureExportCommand.h" #include "Workflows/ConfigurationFlow.h" #include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" @@ -25,6 +26,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -51,7 +53,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp index c575cc349a..3c25c0f4da 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -30,13 +30,13 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureShowCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#show"_liv; } void ConfigureShowCommand::ExecuteInternal(Execution::Context& context) const { context << + VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << CreateConfigurationProcessor << OpenConfigurationSet << diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp index bfc53f78a9..8ced8e83e9 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -30,7 +30,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureTestCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#test"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp index 024b450e7a..9adb7dd82a 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -29,7 +29,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureValidateCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#validate"_liv; } diff --git a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp index f3ae3e1ce0..5bd590c9a8 100644 --- a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp +++ b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -99,7 +99,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // Support for 0.2 schema via metadata value // TODO: Support case insensitive lookup by iteration auto unitMetadata = unit.Metadata(); - auto securityContext = unitMetadata.TryLookup(L"SecurityContext"); + auto securityContext = unitMetadata.TryLookup(L"securityContext"); if (securityContext) { auto securityContextProperty = securityContext.try_as(); diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp new file mode 100644 index 0000000000..2a9fff3555 --- /dev/null +++ b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigureExportCommand.h" +#include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" + +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + std::vector ConfigureExportCommand::GetArguments() const + { + return { + Argument{ Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, true }, + Argument{ Execution::Args::Type::ConfigurationExportPackageId, Resource::String::ConfigureExportPackageId }, + Argument{ Execution::Args::Type::ConfigurationExportModule, Resource::String::ConfigureExportModule }, + Argument{ Execution::Args::Type::ConfigurationExportResource, Resource::String::ConfigureExportResource }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath }, + }; + } + + Resource::LocString ConfigureExportCommand::ShortDescription() const + { + return { Resource::String::ConfigureExportCommandShortDescription }; + } + + Resource::LocString ConfigureExportCommand::LongDescription() const + { + return { Resource::String::ConfigureExportCommandLongDescription }; + } + + Utility::LocIndView ConfigureExportCommand::HelpLink() const + { + return "https://aka.ms/winget-command-configure#export"_liv; + } + + void ConfigureExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + VerifyIsFullPackage << + CreateConfigurationProcessor << + CreateOrOpenConfigurationSet << + AddWinGetPackageAndResource << + WriteConfigFile; + } + + void ConfigureExportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + + bool validInputArgs = false; + if (execArgs.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource) || + execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + validInputArgs = true; + } + + if (!validInputArgs) + { + throw CommandException(Resource::String::ConfigureExportArgumentError); + } + } +} diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.h b/src/AppInstallerCLICore/ConfigureExportCommand.h new file mode 100644 index 0000000000..a8a7895790 --- /dev/null +++ b/src/AppInstallerCLICore/ConfigureExportCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct ConfigureExportCommand final : public Command + { + ConfigureExportCommand(std::string_view parent) : Command("export", parent, Settings::ExperimentalFeature::Feature::ConfigureExport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + }; +} diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index c7ad4cf4dc..12d57450e6 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -79,7 +79,6 @@ namespace AppInstaller::CLI::Execution Position, // Export Command - OutputFile, IncludeVersions, // Import Command @@ -128,6 +127,9 @@ namespace AppInstaller::CLI::Execution ConfigurationEnable, ConfigurationDisable, ConfigurationModulePath, + ConfigurationExportPackageId, + ConfigurationExportModule, + ConfigurationExportResource, // Common arguments NoVT, // Disable VirtualTerminal outputs @@ -140,6 +142,7 @@ namespace AppInstaller::CLI::Execution Wait, // Prompts the user to press any key before exiting OpenLogs, // Opens the default logs directory after executing the command Force, // Forces the execution of the workflow with non security related issues + OutputFile, DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header @@ -161,7 +164,11 @@ namespace AppInstaller::CLI::Execution Max }; - bool Contains(Type arg) const { return (m_parsedArgs.count(arg) != 0); } + template), bool> = true> + bool Contains(T... arg) const + { + return (... && (m_parsedArgs.count(arg) != 0)); + } const std::vector* GetArgs(Type arg) const { diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp index 12feadf263..97d82f63ca 100644 --- a/src/AppInstallerCLICore/ExecutionContext.cpp +++ b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -487,6 +487,24 @@ namespace AppInstaller::CLI::Execution } #endif + void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action) + { + switch (action) + { + case EnumBasedVariantMapAction::Add: + AICLI_LOG(Workflow, Info, << "Setting data item: " << data); + break; + case EnumBasedVariantMapAction::Contains: + AICLI_LOG(Workflow, Info, << "Checking data item: " << data); + break; + case EnumBasedVariantMapAction::Get: + AICLI_LOG(Workflow, Info, << "Getting data item: " << data); + break; + } + + UNREFERENCED_PARAMETER(map); + } + std::string Context::GetResumeId() { return m_checkpointManager->GetResumeId(); diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index b686e48242..a5fa557570 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -85,10 +85,13 @@ namespace AppInstaller::CLI::Execution bool WaitForAppShutdownEvent(); #endif + // Callback to log data actions. + void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action); + // The context within which all commands execute. // Contains input/output via Execution::Reporter and // arguments via Execution::Args. - struct Context : EnumBasedVariantMap + struct Context : EnumBasedVariantMap { Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 4c922fb2d6..5d5268ef07 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -67,6 +67,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportAddingToFile); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToGetDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); @@ -79,6 +82,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileInvalidYAML); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileVersionUnknown); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingDetails); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingResourceSettings); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInform); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInitializing); @@ -128,8 +132,18 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarning); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningPromptApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningPromptTest); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningSetViewTruncated); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningValueTruncated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportModule); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportPackageId); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportResource); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitInstallDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureTestCommandLongDescription); @@ -550,6 +564,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListAdditionalSource); @@ -558,12 +573,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListData); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListField); WINGET_DEFINE_RESOURCE_STRINGID(SourceListIdentifier); WINGET_DEFINE_RESOURCE_STRINGID(SourceListName); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoneFound); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoSources); - WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(SourceListType); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdated); @@ -578,7 +593,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveOne); WINGET_DEFINE_RESOURCE_STRINGID(SourceRequiresAuthentication); - WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 58736ea4fb..b46c8429ad 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -33,12 +33,21 @@ namespace AppInstaller::CLI::Workflow } #endif - namespace + namespace anon { constexpr std::wstring_view s_Directive_Description = L"description"; constexpr std::wstring_view s_Directive_Module = L"module"; constexpr std::wstring_view s_Directive_AllowPrerelease = L"allowPrerelease"; + constexpr std::wstring_view s_Unit_WinGetPackage = L"WinGetPackage"; + + constexpr std::wstring_view s_Module_WinGetClient = L"Microsoft.WinGet.DSC"; + + constexpr std::wstring_view s_Setting_Id = L"id"; + constexpr std::wstring_view s_Setting_Source = L"source"; + + constexpr std::wstring_view s_WinGetSource = L"winget"; + Logging::Level ConvertLevel(DiagnosticLevel level) { switch (level) @@ -106,7 +115,7 @@ namespace AppInstaller::CLI::Workflow return factory; } - std::optional GetValueSetString(const ValueSet& valueSet, std::wstring_view value) + winrt::hstring GetValueSetString(const ValueSet& valueSet, std::wstring_view value) { if (valueSet.HasKey(value)) { @@ -114,7 +123,7 @@ namespace AppInstaller::CLI::Workflow IPropertyValue property = object.try_as(); if (property && property.Type() == PropertyType::String) { - return Utility::LocIndString{ Utility::ConvertToUTF8(property.GetString()) }; + return property.GetString(); } } @@ -136,259 +145,344 @@ namespace AppInstaller::CLI::Workflow return {}; } - void OutputPropertyValue(OutputStream& out, const IPropertyValue property) + // Contains the output functions and tracks whether any fields needed to be truncated. + struct OutputHelper { - switch (property.Type()) - { - case PropertyType::String: - out << ' ' << Utility::ConvertToUTF8(property.GetString()) << '\n'; - break; - case PropertyType::Boolean: - out << ' ' << (property.GetBoolean() ? Utility::LocIndView("true") : Utility::LocIndView("false")) << '\n'; - break; - case PropertyType::Int64: - out << ' ' << property.GetInt64() << '\n'; - break; - default: - out << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; - break; + OutputHelper(Execution::Context& context) : m_context(context) {} + + size_t ValuesTruncated = 0; + + // Converts a string from the configuration API surface for output. + // All strings coming from the API are external data and not localizable by us. + Utility::LocIndString ConvertForOutput(const std::string& input, size_t maxLines) + { + bool truncated = false; + auto lines = Utility::SplitIntoLines(input); + + if (maxLines == 1 && lines.size() > 1) + { + // If the limit was one line, don't allow line breaks but do allow a second line of overflow + lines.resize(1); + maxLines = 2; + truncated = true; + } + + if (Utility::LimitOutputLines(lines, GetConsoleWidth(), maxLines)) + { + truncated = true; + } + + if (truncated) + { + ++ValuesTruncated; + } + + return Utility::LocIndString{ Utility::Join("\n", lines) }; + } + + Utility::LocIndString ConvertForOutput(const winrt::hstring& input, size_t maxLines) + { + return ConvertForOutput(Utility::ConvertToUTF8(input), maxLines); } - } - void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent); + Utility::LocIndString ConvertIdentifier(const winrt::hstring& input) + { + return ConvertForOutput(input, 1); + } - void OutputValueSetAsArray(OutputStream& out, const ValueSet& valueSetArray, size_t indent) - { - Utility::LocIndString indentString{ std::string(indent, ' ') }; + Utility::LocIndString ConvertURI(const winrt::hstring& input) + { + return ConvertForOutput(input, 1); + } + + Utility::LocIndString ConvertValue(const winrt::hstring& input) + { + return ConvertForOutput(input, 5); + } + + Utility::LocIndString ConvertDetailsIdentifier(const winrt::hstring& input) + { + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 1); + } + + Utility::LocIndString ConvertDetailsURI(const winrt::hstring& input) + { + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 1); + } - std::vector> arrayValues; - for (const auto& arrayValue : valueSetArray) + Utility::LocIndString ConvertDetailsValue(const winrt::hstring& input) { - if (arrayValue.Key() != L"treatAsArray") + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 5); + } + + void OutputValueWithTruncationWarningIfNeeded(const winrt::hstring& input) + { + size_t truncatedBefore = ValuesTruncated; + m_context.Reporter.Info() << ConvertValue(input) << '\n'; + + if (ValuesTruncated > truncatedBefore) { - arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + m_context.Reporter.Warn() << Resource::String::ConfigurationWarningValueTruncated << std::endl; } } - std::sort( - arrayValues.begin(), - arrayValues.end(), - [](const std::pair& a, const std::pair& b) + void OutputPropertyValue(const IPropertyValue property) + { + switch (property.Type()) { - return a.first < b.first; - }); + case PropertyType::String: + m_context.Reporter.Info() << ' '; + OutputValueWithTruncationWarningIfNeeded(property.GetString()); + break; + case PropertyType::Boolean: + m_context.Reporter.Info() << ' ' << (property.GetBoolean() ? Utility::LocIndView("true") : Utility::LocIndView("false")) << '\n'; + break; + case PropertyType::Int64: + m_context.Reporter.Info() << ' ' << property.GetInt64() << '\n'; + break; + default: + m_context.Reporter.Info() << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; + break; + } + } - for (const auto& arrayValue : arrayValues) + void OutputValueSetAsArray(const ValueSet& valueSetArray, size_t indent) { - auto arrayObject = arrayValue.second; - IPropertyValue arrayProperty = arrayObject.try_as(); + Utility::LocIndString indentString{ std::string(indent, ' ') }; - out << indentString << "-"; - if (arrayProperty) + std::vector> arrayValues; + for (const auto& arrayValue : valueSetArray) { - OutputPropertyValue(out, arrayProperty); + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } } - else - { - ValueSet arraySubset = arrayObject.as(); - auto size = arraySubset.Size(); - if (size > 0) + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair& a, const std::pair& b) { - // First one is special. - auto first = arraySubset.First().Current(); - out << ' ' << Utility::ConvertToUTF8(first.Key()) << ':'; - - auto object = first.Value(); - IPropertyValue property = object.try_as(); - if (property) - { - OutputPropertyValue(out, property); - } - else - { - // If not an IPropertyValue, it must be a ValueSet - ValueSet subset = object.as(); - out << '\n'; - OutputValueSet(out, subset, indent + 4); - } + return a.first < b.first; + }); - if (size > 1) + for (const auto& arrayValue : arrayValues) + { + auto arrayObject = arrayValue.second; + IPropertyValue arrayProperty = arrayObject.try_as(); + + m_context.Reporter.Info() << indentString << "-"; + if (arrayProperty) + { + OutputPropertyValue(arrayProperty); + } + else + { + ValueSet arraySubset = arrayObject.as(); + auto size = arraySubset.Size(); + if (size > 0) { - arraySubset.Remove(first.Key()); - OutputValueSet(out, arraySubset, indent + 2); - arraySubset.Insert(first.Key(), first.Value()); + // First one is special. + auto first = arraySubset.First().Current(); + m_context.Reporter.Info() << ' ' << ConvertIdentifier(first.Key()) << ':'; + + auto object = first.Value(); + IPropertyValue property = object.try_as(); + if (property) + { + OutputPropertyValue(property); + } + else + { + // If not an IPropertyValue, it must be a ValueSet + ValueSet subset = object.as(); + m_context.Reporter.Info() << '\n'; + OutputValueSet(subset, indent + 4); + } + + if (size > 1) + { + arraySubset.Remove(first.Key()); + OutputValueSet(arraySubset, indent + 2); + arraySubset.Insert(first.Key(), first.Value()); + } } } } } - } - - void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent) - { - Utility::LocIndString indentString{ std::string(indent, ' ') }; - for (const auto& value : valueSet) + void OutputValueSet(const ValueSet& valueSet, size_t indent) { - out << indentString << Utility::ConvertToUTF8(value.Key()) << ':'; - - auto object = value.Value(); + Utility::LocIndString indentString{ std::string(indent, ' ') }; - IPropertyValue property = object.try_as(); - if (property) - { - OutputPropertyValue(out, property); - } - else + for (const auto& value : valueSet) { - // If not an IPropertyValue, it must be a ValueSet - ValueSet subset = object.as(); - out << '\n'; - if (subset.HasKey(L"treatAsArray")) + m_context.Reporter.Info() << indentString << ConvertIdentifier(value.Key()) << ':'; + + auto object = value.Value(); + + IPropertyValue property = object.try_as(); + if (property) { - OutputValueSetAsArray(out, subset, indent + 2); + OutputPropertyValue(property); } else { - OutputValueSet(out, subset, indent + 2); + // If not an IPropertyValue, it must be a ValueSet + ValueSet subset = object.as(); + m_context.Reporter.Info() << '\n'; + if (subset.HasKey(L"treatAsArray")) + { + OutputValueSetAsArray(subset, indent + 2); + } + else + { + OutputValueSet(subset, indent + 2); + } } } } - } - // Converts a string from the configuration API surface for output. - // All strings coming from the API are external data and not localizable by us. - Utility::LocIndString ConvertForOutput(const winrt::hstring& input) - { - return Utility::LocIndString{ Utility::ConvertToUTF8(input) }; - } + void OutputConfigurationUnitHeader(const ConfigurationUnit& unit, const winrt::hstring& name) + { + m_context.Reporter.Info() << ConfigurationIntentEmphasis << ToResource(unit.Intent()) << " :: "_liv << ConfigurationUnitEmphasis << ConvertIdentifier(name); - void OutputConfigurationUnitHeader(OutputStream& out, const ConfigurationUnit& unit, const winrt::hstring& name) - { - out << ConfigurationIntentEmphasis << ToResource(unit.Intent()) << " :: "_liv << ConfigurationUnitEmphasis << ConvertForOutput(name); + winrt::hstring identifier = unit.Identifier(); + if (!identifier.empty()) + { + m_context.Reporter.Info() << " ["_liv << ConvertIdentifier(identifier) << ']'; + } - winrt::hstring identifier = unit.Identifier(); - if (!identifier.empty()) - { - out << " ["_liv << ConvertForOutput(identifier) << ']'; + m_context.Reporter.Info() << '\n'; } - out << '\n'; - } + void OutputConfigurationUnitInformation(const ConfigurationUnit& unit) + { + IConfigurationUnitProcessorDetails details = unit.Details(); + ValueSet metadata = unit.Metadata(); - void OutputConfigurationUnitInformation(OutputStream& out, const ConfigurationUnit& unit) - { - IConfigurationUnitProcessorDetails details = unit.Details(); - ValueSet metadata = unit.Metadata(); - - if (details) - { - // -- Sample output when IConfigurationUnitProcessorDetails present -- - // Intent :: UnitType [Identifier] - // UnitDocumentationUri - // Description - // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) - // "Signed by": SigningCertificateChain (leaf subject CN) - // PublishedModuleUri / ModuleDocumentationUri - // ModuleDescription - OutputConfigurationUnitHeader(out, unit, details.UnitType()); - - auto unitDocumentationUri = details.UnitDocumentationUri(); - if (unitDocumentationUri) + if (details) { - out << " "_liv << ConvertForOutput(unitDocumentationUri.DisplayUri()) << '\n'; - } + // -- Sample output when IConfigurationUnitProcessorDetails present -- + // Intent :: UnitType [Identifier] + // UnitDocumentationUri + // Description + // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) + // "Signed by": SigningCertificateChain (leaf subject CN) + // PublishedModuleUri / ModuleDocumentationUri + // ModuleDescription + OutputConfigurationUnitHeader(unit, details.UnitType()); + + auto unitDocumentationUri = details.UnitDocumentationUri(); + if (unitDocumentationUri) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsURI(unitDocumentationUri.DisplayUri()) << '\n'; + } - winrt::hstring unitDescriptionFromDetails = details.UnitDescription(); - if (!unitDescriptionFromDetails.empty()) - { - out << " "_liv << ConvertForOutput(unitDescriptionFromDetails) << '\n'; - } - else - { - auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); - if (unitDescriptionFromDirectives && !unitDescriptionFromDirectives.value().empty()) + winrt::hstring unitDescriptionFromDetails = details.UnitDescription(); + if (!unitDescriptionFromDetails.empty()) { - out << " "_liv << unitDescriptionFromDirectives.value() << '\n'; + m_context.Reporter.Info() << " "_liv << ConvertDetailsValue(unitDescriptionFromDetails) << '\n'; + } + else + { + auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); + if (!unitDescriptionFromDirectives.empty()) + { + m_context.Reporter.Info() << " "_liv; + OutputValueWithTruncationWarningIfNeeded(unitDescriptionFromDirectives); + } } - } - auto author = ConvertForOutput(details.Author()); - if (author.empty()) - { - author = ConvertForOutput(details.Publisher()); - } - if (details.IsLocal()) - { - out << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertForOutput(details.ModuleName()), author, Resource::String::ConfigurationLocal) << '\n'; - } - else - { - out << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertForOutput(details.ModuleName()), author, ConvertForOutput(details.ModuleSource())) << '\n'; - } + auto author = ConvertDetailsIdentifier(details.Author()); + if (author.empty()) + { + author = ConvertDetailsIdentifier(details.Publisher()); + } + if (details.IsLocal()) + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, Resource::String::ConfigurationLocal) << '\n'; + } + else + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, ConvertDetailsIdentifier(details.ModuleSource())) << '\n'; + } - // TODO: Currently the signature information is only for the top files. Maybe each item should be tagged? - // TODO: Output signing information with additional details (like whether the certificate is trusted). Doing this with the validate command - // seems like a good time, as that will also need to do the check in order to inform the user on the validation. - // Just saying "Signed By: Foo" is going to lead to a false sense of trust if the signature is valid but not actually trusted. + // TODO: Currently the signature information is only for the top files. Maybe each item should be tagged? + // TODO: Output signing information with additional details (like whether the certificate is trusted). Doing this with the validate command + // seems like a good time, as that will also need to do the check in order to inform the user on the validation. + // Just saying "Signed By: Foo" is going to lead to a false sense of trust if the signature is valid but not actually trusted. - auto moduleUri = details.PublishedModuleUri(); - if (!moduleUri) - { - moduleUri = details.ModuleDocumentationUri(); + auto moduleUri = details.PublishedModuleUri(); + if (!moduleUri) + { + moduleUri = details.ModuleDocumentationUri(); + } + if (moduleUri) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsURI(moduleUri.DisplayUri()) << '\n'; + } + + winrt::hstring moduleDescription = details.ModuleDescription(); + if (!moduleDescription.empty()) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsValue(moduleDescription) << '\n'; + } } - if (moduleUri) + else { - out << " "_liv << ConvertForOutput(moduleUri.DisplayUri()) << '\n'; - } + // -- Sample output when no IConfigurationUnitProcessorDetails present -- + // Intent :: Type [identifier] + // Description (from directives) + // "Module": module + OutputConfigurationUnitHeader(unit, unit.Type()); + + auto description = GetValueSetString(metadata, s_Directive_Description); + if (!description.empty()) + { + m_context.Reporter.Info() << " "_liv; + OutputValueWithTruncationWarningIfNeeded(description); + } - winrt::hstring moduleDescription = details.ModuleDescription(); - if (!moduleDescription.empty()) - { - out << " "_liv << ConvertForOutput(moduleDescription) << '\n'; + auto module = GetValueSetString(metadata, s_Directive_Module); + if (!module.empty()) + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleNameOnly(ConvertIdentifier(module)) << '\n'; + } } - } - else - { - // -- Sample output when no IConfigurationUnitProcessorDetails present -- - // Intent :: Type [identifier] - // Description (from directives) - // "Module": module - OutputConfigurationUnitHeader(out, unit, unit.Type()); - auto description = GetValueSetString(metadata, s_Directive_Description); - if (description && !description.value().empty()) + // -- Sample output footer -- + // Dependencies: dep1, dep2, ... + // Settings: + // <... settings splat> + auto dependencies = unit.Dependencies(); + if (dependencies.Size() > 0) { - out << " "_liv << description.value() << '\n'; + std::ostringstream allDependencies; + for (const winrt::hstring& dependency : dependencies) + { + allDependencies << ' ' << ConvertIdentifier(dependency); + } + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationDependencies(Utility::LocIndString{ std::move(allDependencies).str() }) << '\n'; } - auto module = GetValueSetString(metadata, s_Directive_Module); - if (module && !module.value().empty()) + ValueSet settings = unit.Settings(); + if (settings.Size() > 0) { - out << " "_liv << Resource::String::ConfigurationModuleNameOnly(module.value()) << '\n'; + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationSettings << '\n'; + OutputValueSet(settings, 4); } } - // -- Sample output footer -- - // Dependencies: dep1, dep2, ... - // Settings: - // <... settings splat> - auto dependencies = unit.Dependencies(); - if (dependencies.Size() > 0) - { - std::ostringstream allDependencies; - for (const winrt::hstring& dependency : dependencies) - { - allDependencies << ' ' << Utility::ConvertToUTF8(dependency); - } - out << " "_liv << Resource::String::ConfigurationDependencies(Utility::LocIndString{ std::move(allDependencies).str() }) << '\n'; - } + private: + Execution::Context& m_context; + }; - ValueSet settings = unit.Settings(); - if (settings.Size() > 0) - { - out << " "_liv << Resource::String::ConfigurationSettings << '\n'; - OutputValueSet(out, settings, 4); - } + void OutputConfigurationUnitHeader(Execution::Context& context, const ConfigurationUnit& unit, const winrt::hstring& name) + { + OutputHelper helper{ context }; + helper.OutputConfigurationUnitHeader(unit, name); } void LogFailedGetConfigurationUnitDetails(const ConfigurationUnit& unit, const IConfigurationUnitResultInformation& resultInformation) @@ -701,8 +795,7 @@ namespace AppInstaller::CLI::Workflow { m_unitsSeen.insert(unitInstance); - OutputStream out = m_context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); + OutputConfigurationUnitHeader(m_context, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); } } @@ -758,10 +851,7 @@ namespace AppInstaller::CLI::Workflow EndProgress(); - { - OutputStream info = m_context.Reporter.Info(); - OutputConfigurationUnitHeader(info, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); - } + OutputConfigurationUnitHeader(m_context, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); switch (testResult) { @@ -837,6 +927,254 @@ namespace AppInstaller::CLI::Workflow return validationOrder; } + + void SetNameAndOrigin(ConfigurationSet& set, std::filesystem::path& absolutePath) + { + // TODO: Consider how to properly determine a good value for name and origin. + set.Name(absolutePath.filename().wstring()); + set.Origin(absolutePath.parent_path().wstring()); + set.Path(absolutePath.wstring()); + } + + void OpenConfigurationSet(Execution::Context& context, const std::string& argPath, bool allowRemote) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); + + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + bool isRemote = Utility::IsUrlRemote(argPath); + std::filesystem::path absolutePath; + Streams::IInputStream inputStream = nullptr; + + if (isRemote) + { + if (!allowRemote) + { + AICLI_LOG(Config, Error, << "Remote files are not supported"); + AICLI_TERMINATE_CONTEXT(ERROR_NOT_SUPPORTED); + } + + std::ostringstream stringStream; + ProgressCallback emptyCallback; + Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); + + auto strContent = stringStream.str(); + std::vector byteContent{ strContent.begin(), strContent.end() }; + + Streams::InMemoryRandomAccessStream memoryStream; + Streams::DataWriter streamWriter{ memoryStream }; + streamWriter.WriteBytes(byteContent); + streamWriter.StoreAsync().get(); + streamWriter.DetachStream(); + memoryStream.Seek(0); + inputStream = memoryStream; + } + else + { + absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + inputStream = openAction.get(); + } + + OpenConfigurationSetResult openResult = nullptr; + { + auto openAction = context.Get().Processor().OpenConfigurationSetAsync(inputStream); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + openResult = openAction.get(); + } + + progressScope.reset(); + + if (FAILED_LOG(static_cast(openResult.ResultCode().value))) + { + AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast(openResult.ResultCode().value)); + + switch (openResult.ResultCode()) + { + case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_MISSING_FIELD: + context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: + context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: + case WINGET_CONFIG_ERROR_INVALID_YAML: + default: + context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; + break; + } + + if (openResult.Line() != 0) + { + context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; + } + + AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); + } + + ConfigurationSet result = openResult.Set(); + + // Temporary block on using schema 0.3 while experimental + if (result.SchemaVersion() == L"0.3") + { + AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); + } + + // Fill out the information about the set based on it coming from a file. + if (isRemote) + { + result.Name(Utility::GetFileNameFromURI(argPath).wstring()); + result.Origin(argPathWide); + // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. + } + else + { + SetNameAndOrigin(result, absolutePath); + } + + context.Get().Set(result); + } + + std::optional CreateWinGetUnit(const Execution::Context& context) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + // Maybe we can add some checks to validate the package id exists. + std::string packageId{ context.Args.GetArg(Args::Type::ConfigurationExportPackageId) }; + std::wstring packageIdWide = Utility::ConvertToUTF16(packageId); + + ConfigurationUnit unit; + unit.Type(s_Unit_WinGetPackage); + unit.Identifier(packageIdWide); + unit.Intent(ConfigurationUnitIntent::Apply); + + auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ packageId }); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(s_Module_WinGetClient)); + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + ValueSet settings; + settings.Insert(s_Setting_Id, PropertyValue::CreateString(packageIdWide)); + settings.Insert(s_Setting_Source, PropertyValue::CreateString(s_WinGetSource)); + unit.Settings(settings); + + return unit; + } + + return {}; + } + + GetConfigurationUnitSettingsResult GetUnitSettings(Execution::Context& context, ConfigurationUnit& unit) + { + // This assumes there are no required properties for Get, but for example WinGetPackage requires the Id. + // It is obviously wrong and will be wrong until Export is implemented for DSC v2 and a proper way to inform + // about input to winget configure export is implemented. Drink the kool-aid and transcend. + unit.Intent(ConfigurationUnitIntent::Inform); + + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationGettingResourceSettings()); + + GetConfigurationUnitSettingsResult getResult = nullptr; + { + auto getAction = context.Get().Processor().GetUnitSettingsAsync(unit); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { getAction.Cancel(); }); + getResult = getAction.get(); + } + + progressScope.reset(); + return getResult; + } + + std::optional CreateConfigurationUnit(Execution::Context& context, const std::optional dependantUnit) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) + { + std::string moduleName{ context.Args.GetArg(Args::Type::ConfigurationExportModule) }; + std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); + + std::string resourceName{ context.Args.GetArg(Args::Type::ConfigurationExportResource) }; + std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); + + ConfigurationUnit unit; + unit.Type(resourceNameWide); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + + Utility::LocIndString description; + if (dependantUnit.has_value()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependantUnit.value().Identifier()) }); + } + else + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); + } + + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + // Call processor to get settings for the unit. + auto getResult = GetUnitSettings(context, unit); + winrt::hresult resultCode = getResult.ResultInformation().ResultCode(); + if (FAILED(resultCode)) + { + // Retry if it fails with not found in the case the module is a pre-released one. + bool isPreRelease = false; + if (resultCode == WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY) + { + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + auto preReleaseResult = GetUnitSettings(context, unit); + if (SUCCEEDED(preReleaseResult.ResultInformation().ResultCode())) + { + isPreRelease = true; + getResult = preReleaseResult; + } + else + { + AICLI_LOG(Config, Error, << "Failed Get allowing prerelease modules"); + LogFailedGetConfigurationUnitDetails(unit, preReleaseResult.ResultInformation()); + } + } + + if (!isPreRelease) + { + OutputUnitRunFailure(context, unit, getResult.ResultInformation()); + THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + } + } + + unit.Settings(getResult.Settings()); + + // GetUnitSettings will set it to Inform. + unit.Intent(ConfigurationUnitIntent::Apply); + + // Add dependency if needed. + if (dependantUnit.has_value()) + { + auto dependencies = winrt::single_threaded_vector(); + dependencies.Append(dependantUnit.value().Identifier()); + unit.Dependencies(std::move(dependencies)); + } + + return unit; + } + + return {}; + } } void CreateConfigurationProcessor(Context& context) @@ -844,10 +1182,10 @@ namespace AppInstaller::CLI::Workflow auto progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationInitializing()); - ConfigurationProcessor processor{ CreateConfigurationSetProcessorFactory(context)}; + ConfigurationProcessor processor{ anon::CreateConfigurationSetProcessorFactory(context)}; // Set the processor to the current level of the logging. - processor.MinimumLevel(ConvertLevel(Logging::Log().GetLevel())); + processor.MinimumLevel(anon::ConvertLevel(Logging::Log().GetLevel())); processor.Caller(L"winget"); // Use same activity as the overall winget command processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); @@ -857,7 +1195,7 @@ namespace AppInstaller::CLI::Workflow // Route the configuration diagnostics into the context's diagnostics logging processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const IDiagnosticInformation& diagnostics) { - context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); + context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); }); ConfigurationContext configurationContext; @@ -868,106 +1206,30 @@ namespace AppInstaller::CLI::Workflow void OpenConfigurationSet(Context& context) { - auto progressScope = context.Reporter.BeginAsyncProgress(true); - progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); - std::string argPath{ context.Args.GetArg(Args::Type::ConfigurationFile) }; - std::wstring argPathWide = Utility::ConvertToUTF16(argPath); - bool isRemote = Utility::IsUrlRemote(argPath); - std::filesystem::path absolutePath; - Streams::IInputStream inputStream = nullptr; + anon::OpenConfigurationSet(context, argPath, true); + } - if (isRemote) - { - std::ostringstream stringStream; - ProgressCallback emptyCallback; - Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); - - auto strContent = stringStream.str(); - std::vector byteContent{ strContent.begin(), strContent.end() }; - - Streams::InMemoryRandomAccessStream memoryStream; - Streams::DataWriter streamWriter{ memoryStream }; - streamWriter.WriteBytes(byteContent); - streamWriter.StoreAsync().get(); - streamWriter.DetachStream(); - memoryStream.Seek(0); - inputStream = memoryStream; - } - else - { - absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); - auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - inputStream = openAction.get(); - } + void CreateOrOpenConfigurationSet(Context& context) + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; - OpenConfigurationSetResult openResult = nullptr; + if (std::filesystem::exists(argPath)) { - auto openAction = context.Get().Processor().OpenConfigurationSetAsync(inputStream); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - openResult = openAction.get(); + anon::OpenConfigurationSet(context, argPath, false); } - - progressScope.reset(); - - if (FAILED_LOG(static_cast(openResult.ResultCode().value))) + else { - AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast(openResult.ResultCode().value)); - - switch (openResult.ResultCode()) - { - case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_MISSING_FIELD: - context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: - context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: - case WINGET_CONFIG_ERROR_INVALID_YAML: - default: - context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; - break; - } - - if (openResult.Line() != 0) - { - context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; - } + // TODO: support other schema versions or pick up latest. + ConfigurationSet set; + set.SchemaVersion(L"0.2"); - AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); - } - - ConfigurationSet result = openResult.Set(); + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + anon::SetNameAndOrigin(set, absolutePath); - // Temporary block on using schema 0.3 while experimental - if (result.SchemaVersion() == L"0.3") - { - AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); + context.Get().Set(set); } - - // Fill out the information about the set based on it coming from a file. - if (isRemote) - { - result.Name(Utility::GetFileNameFromURI(argPath).wstring()); - result.Origin(argPathWide); - // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. - } - else - { - // TODO: Consider how to properly determine a good value for name and origin. - result.Name(absolutePath.filename().wstring()); - result.Origin(absolutePath.parent_path().wstring()); - result.Path(absolutePath.wstring()); - } - - context.Get().Set(result); } void ShowConfigurationSet(Context& context) @@ -986,9 +1248,9 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::ReadOnly); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); - OutputStream out = context.Reporter.Info(); + anon::OutputHelper outputHelper{ context }; uint32_t unitsShown = 0; getDetailsOperation.Progress([&](const IAsyncOperationWithProgress& operation, const GetConfigurationUnitDetailsResult&) @@ -1001,8 +1263,8 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); - OutputConfigurationUnitInformation(out, unitResult.Unit()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } progressScope = context.Reporter.BeginAsyncProgress(true); @@ -1046,8 +1308,8 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); - OutputConfigurationUnitInformation(out, unitResult.Unit()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } } } @@ -1057,7 +1319,13 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < allUnits.Size(); ++unitsShown) { ConfigurationUnit unit = allUnits.GetAt(unitsShown); - OutputConfigurationUnitInformation(out, unit); + outputHelper.OutputConfigurationUnitInformation(unit); + } + + if (outputHelper.ValuesTruncated) + { + // Using error to make this stand out from other warnings + context.Reporter.Error() << Resource::String::ConfigurationWarningSetViewTruncated << std::endl; } } @@ -1093,7 +1361,7 @@ namespace AppInstaller::CLI::Workflow { auto applyOperation = configContext.Processor().ApplySetAsync(configContext.Set(), ApplyConfigurationSetFlags::None); - ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; + anon::ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; result = applyOperation.get(); progress.HandleUnreportedProgress(result); @@ -1120,7 +1388,7 @@ namespace AppInstaller::CLI::Workflow { auto testOperation = configContext.Processor().TestSetAsync(configContext.Set()); - TestConfigurationSetProgressOutput progress{ context, testOperation }; + anon::TestConfigurationSetProgressOutput progress{ context, testOperation }; result = testOperation.get(); progress.HandleUnreportedProgress(result); @@ -1183,8 +1451,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationUnit unit = unitResult.Unit(); - auto out = context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); switch (resultCode) { @@ -1220,7 +1487,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getLocalDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Local); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); HRESULT getLocalHR = S_OK; GetConfigurationSetDetailsResult getLocalResult = nullptr; @@ -1254,7 +1521,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getCatalogDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Catalog); - unification = CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); + unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); HRESULT getCatalogHR = S_OK; GetConfigurationSetDetailsResult getCatalogResult = nullptr; @@ -1314,14 +1581,14 @@ namespace AppInstaller::CLI::Workflow { if (needsHeader) { - auto out = context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); + needsHeader = false; foundIssue = true; } }; - if (GetValueSetString(unit.Metadata(), s_Directive_Module).value_or(Utility::LocIndString{}).empty()) + if (anon::GetValueSetString(unit.Metadata(), anon::s_Directive_Module).empty()) { outputHeaderIfNeeded(); context.Reporter.Warn() << " "_liv << Resource::String::ConfigurationUnitModuleNotProvidedWarning << std::endl; @@ -1344,23 +1611,23 @@ namespace AppInstaller::CLI::Workflow if (FAILED(catalogUnitResult.ResultInformation().ResultCode())) { outputHeaderIfNeeded(); - OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); + anon::OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); continue; } // If not already prerelease, try with prerelease and warn if found - std::optional allowPrereleaseDirective = GetValueSetBool(unit.Metadata(), s_Directive_AllowPrerelease); + std::optional allowPrereleaseDirective = anon::GetValueSetBool(unit.Metadata(), anon::s_Directive_AllowPrerelease); if (!allowPrereleaseDirective || !allowPrereleaseDirective.value()) { // Check if the configuration unit is prerelease but the author forgot it ConfigurationUnit clone = unit.Copy(); - clone.Metadata().Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + clone.Metadata().Insert(anon::s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(gettingDetailString); auto getUnitDetailsOperation = configContext.Processor().GetUnitDetailsAsync(clone, ConfigurationUnitDetailFlags::Catalog); - auto unitUnification = CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); + auto unitUnification = anon::CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); IConfigurationUnitProcessorDetails prereleaseDetails; @@ -1404,7 +1671,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationContext& configContext = context.Get(); auto units = configContext.Set().Units(); - auto validationOrder = GetConfigurationSetUnitValidationOrder(units.GetView()); + auto validationOrder = anon::GetConfigurationSetUnitValidationOrder(units.GetView()); Configuration::WingetDscModuleUnitValidator wingetUnitValidator; @@ -1434,4 +1701,58 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::ConfigurationValidationFoundNoIssues << std::endl; } + + void AddWinGetPackageAndResource(Execution::Context& context) + { + auto wingetUnit = anon::CreateWinGetUnit(context); + auto configUnit = anon::CreateConfigurationUnit(context, wingetUnit); + + ConfigurationContext& configContext = context.Get(); + if (wingetUnit.has_value()) + { + configContext.Set().Units().Append(wingetUnit.value()); + } + + if (configUnit.has_value()) + { + configContext.Set().Units().Append(configUnit.value()); + } + } + + void WriteConfigFile(Execution::Context& context) + { + try + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; + + context.Reporter.Info() << Resource::String::ConfigurationExportAddingToFile(Utility::LocIndView{ argPath }) << std::endl; + + auto tempFilePath = Runtime::GetNewTempFilePath(); + + { + std::ofstream tempStream{ tempFilePath }; + tempStream << "# Created using winget configure export " << Runtime::GetClientVersion().get() << std::endl; + } + + auto openAction = Streams::FileRandomAccessStream::OpenAsync( + tempFilePath.wstring(), + FileAccessMode::ReadWrite); + + auto stream = openAction.get(); + stream.Seek(stream.Size()); + + ConfigurationContext& configContext = context.Get(); + configContext.Set().Serialize(openAction.get()); + + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPath }); + std::filesystem::rename(tempFilePath, absolutePath); + + context.Reporter.Info() << Resource::String::ConfigurationExportSuccessful << std::endl; + } + catch (...) + { + context.Reporter.Error() << Resource::String::ConfigurationExportFailed << std::endl; + throw; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h index d1b50fc104..274cb3fa31 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -17,6 +17,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: ConfigurationSet void OpenConfigurationSet(Execution::Context& context); + // Creates or opens the configuration set. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor + // Outputs: ConfigurationSet + void CreateOrOpenConfigurationSet(Execution::Context& context); + // Outputs the configuration set. // Required Args: None // Inputs: ConfigurationSet @@ -84,4 +90,16 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ValidateAllGoodMessage(Execution::Context& context); + + // Adds a configuration unit with the winget package and/or exports resource given. + // Required Args: None + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void AddWinGetPackageAndResource(Execution::Context& context); + + // Write the configuration file. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void WriteConfigFile(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index 701173b52a..c1f75cdaaa 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -146,6 +146,17 @@ namespace AppInstaller::CLI::Workflow try { std::filesystem::remove(path); + + // It is assumed that the parent of the installer path will always be a directory + // If it isn't, then something went severely wrong. However, we will check that + // it is a directory here just to be safe. If it is an empty directory, remove it + + if (std::filesystem::is_directory(path.parent_path()) && + std::filesystem::is_empty(path.parent_path()) + ) + { + std::filesystem::remove(path.parent_path()); + } } catch (const std::exception& e) { diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 752455b7d8..dbf3e754bf 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -13,6 +13,8 @@ #include #include +EXTERN_C IMAGE_DOS_HEADER __ImageBase; + using namespace std::string_literals; using namespace AppInstaller::Utility::literals; using namespace AppInstaller::Pinning; @@ -259,6 +261,19 @@ namespace AppInstaller::CLI::Workflow m_func(context); } + void WorkflowTask::Log() const + { + if (m_isFunc) + { + // Using `00000001`80000000` as base address default when loading dll into windbg as dump file. + AICLI_LOG(Workflow, Info, << "Running task: 0x" << m_func << " [ln 00000001`80000000+" << std::hex << (reinterpret_cast(m_func) - reinterpret_cast(&__ImageBase)) << "]"); + } + else + { + AICLI_LOG(Workflow, Info, << "Running task: " << m_name); + } + } + Repository::PredefinedSource DetermineInstalledSource(const Execution::Context& context) { Repository::PredefinedSource installedSource = Repository::PredefinedSource::Installed; @@ -1368,10 +1383,10 @@ namespace AppInstaller::CLI::Workflow void GetInstalledPackageVersion(Execution::Context& context) { - std::shared_ptr installed = context.Get()->GetInstalled(); - if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) { + std::shared_ptr installed = context.Get()->GetInstalled(); + if (installed) { // TODO: This may need to be expanded dramatically to enable targeting across a variety of dimensions (architecture, etc.) @@ -1395,6 +1410,10 @@ namespace AppInstaller::CLI::Workflow context.Add(installed->GetLatestVersion()); } } + else + { + context.Add(nullptr); + } } else { @@ -1433,6 +1452,7 @@ AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution:: if (context.ShouldExecuteWorkflowTask(task)) #endif { + task.Log(); task(context); } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index c8cdce1f2c..a923c8dd26 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -69,6 +69,7 @@ namespace AppInstaller::CLI::Workflow bool IsFunction() const { return m_isFunc; } Func Function() const { return m_func; } bool ExecuteAlways() const { return m_executeAlways; } + void Log() const; private: bool m_isFunc = false; diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs index 115fa83d24..66fdc60571 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -120,5 +120,18 @@ public void ShowDetailsFromHttpsConfigurationFile() Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains(Constants.TestRepoName)); } + + /// + /// This test ensures that there is not significant overflow from large strings in the configuration file. + /// + [Test] + public void ShowTruncatedDetailsAndFileContent() + { + var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\LargeContentStrings.yml")} --verbose"); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains("")); + Assert.True(result.StdOut.Contains("Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content.")); + Assert.False(result.StdOut.Contains("Line5")); + } } } diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index e2acb23d9b..e63e6393df 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -307,6 +307,7 @@ public class ErrorCode public const int CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public const int CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public const int CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public const int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); public const int CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); public const int CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY = unchecked((int)0x8A15C102); diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml new file mode 100644 index 0000000000..a5971d708d --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml @@ -0,0 +1,22 @@ +properties: + configurationVersion: 0.2 + resources: + - resource: xE2EMalicious/E2EMalicious + id: firstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirst + directives: + repository: AppInstallerCLIE2ETestsRepo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + settings: + key: Foo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + - resource: Unknown + dependsOn: + - firstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirst + directives: + module: UnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknown + repository: AppInstallerCLIE2ETestsRepo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + settings: + Path: ${WinGetConfigRoot}\ConfigServerUnexpectedExit.txt + Content: Contents! + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 new file mode 100644 index 0000000000..fcf009be10 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 @@ -0,0 +1,33 @@ +# +# Module manifest for module 'xE2ETestResource' +# + +@{ + +RootModule = 'xE2EMalicious.psm1' +ModuleVersion = '0.0.0.1' +GUID = 'a0be43e8-ac22-4244-8efc-7263dfa50b92' +CompatiblePSEditions = 'Core' +Author = "WinGet Dev Team" +CompanyName = 'Microsoft Corporation' +Copyright = '(c) Microsoft Corporation. All rights reserved.' +Description = "PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests" +PowerShellVersion = '7.2' +FunctionsToExport = @() +CmdletsToExport = @() +DscResourcesToExport = @( + 'E2EMalicious' +) +HelpInfoURI = 'https://www.contoso.com/help' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + ProjectUri = 'https://github.com/microsoft/winget-cli' + IconUri = 'https://www.contoso.com/icons/icon.png' + } + +} + +} diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 new file mode 100644 index 0000000000..603827fb55 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 @@ -0,0 +1,86 @@ +# E2E module with resources. + +enum Ensure +{ + Absent + Present +} + +# This resource just checks if a file is there or not with and if its with the specified content. +[DscResource()] +class E2EMalicious +{ + [DscProperty(Key)] + [string] $Path + + [DscProperty()] + [Ensure] $Ensure = [Ensure]::Present + + [DscProperty()] + [string] $Content = $null + + [E2EFileResource] Get() + { + if ([string]::IsNullOrEmpty($this.Path)) + { + throw + } + + $fileContent = $null + if (Test-Path -Path $this.Path -PathType Leaf) + { + $fileContent = Get-Content $this.Path -Raw + } + + $result = @{ + Path = $this.Path + Content = $fileContent + } + + return $result + } + + [bool] Test() + { + $get = $this.Get() + + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + return $this.Content -eq $get.Content + } + } + elseif ($this.Ensure -eq [Ensure]::Absent) + { + return $true + } + + return $false + } + + [void] Set() + { + if (-not $this.Test()) + { + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + else + { + Remove-Item $this.Path + } + } + else + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + } + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index c61acd47b1..9e7f493d4f 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2956,4 +2956,55 @@ Please specify one of them using the --source option to proceed. Select the target platform + + Adding configuration file: {0} + {Locked="{0}"} + + + Successfully exported + + + Getting configuration settings... + + + At least --packageId and/or --module with --resource must be provided + {Locked="--packageId,--module, --resource"} + + + Exports configuration resources to a configuration file. When used with --packageId, exports a WinGetPackage resource of the given package id. When used with --module and --resource, gets the settings of the resource and exports it to the configuration file. If the output configuration file already exists, appends the exported configuration resources. + {Locked="WinGetPackage,--packageId,--module, --resource"} + + + Exports configuration resources to a configuration file. + + + The module of the resource to export. + + + The package identifier to export. + + + The configuration resource to export. + + + The configuration unit failed getting its properties. + + + Failed exporting configuration. + + + Configure {0} + {Locked="{0}"} + + + Install {0} + {Locked="{0}"} + + + Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content. + + + <this value has been truncated; inspect the file contents for the complete text> + Keep some form of separator like the "<>" around the text so that it stands out from the preceding text. + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index aeea7f999a..6e372802b0 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -326,6 +326,7 @@ + @@ -933,40 +934,34 @@ true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true + + + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 0b04b135ad..1159b9e350 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -350,6 +350,9 @@ Source Files\Common + + Source Files\Common + @@ -984,5 +987,8 @@ TestData + + TestData + \ No newline at end of file diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index a28b45451a..68214f4370 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -249,7 +249,7 @@ TEST_CASE("Format", "[strings]") REQUIRE("First {1}" == Format("{0} {1}", "First")); } -TEST_CASE("SplitIntoLines", "[string]") +TEST_CASE("SplitIntoLines", "[strings]") { REQUIRE(SplitIntoLines("Boring test") == std::vector{ "Boring test" }); REQUIRE(SplitIntoLines( @@ -261,7 +261,7 @@ TEST_CASE("SplitIntoLines", "[string]") == std::vector{ "You want my treasure?", "You can have it!", "I left everything I gathered in one place!", "You just have to find it!" }); } -TEST_CASE("SplitWithSeparator", "[string]") +TEST_CASE("SplitWithSeparator", "[strings]") { std::vector test1 = Split("first;second;third", ';'); REQUIRE(test1.size() == 3); @@ -285,10 +285,40 @@ TEST_CASE("SplitWithSeparator", "[string]") REQUIRE(test4[1] == "spaces"); } -TEST_CASE("ConvertGuid", "[string]") +TEST_CASE("ConvertGuid", "[strings]") { std::string validGuidString = "{4d1e55b2-f16f-11cf-88cb-001111000030}"; GUID guid = { 0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; REQUIRE(CaseInsensitiveEquals(ConvertGuidToString(guid), validGuidString)); } + +TEST_CASE("FindControlCodeToConvert", "[strings]") +{ + REQUIRE(FindControlCodeToConvert("No codes") == std::string::npos); + REQUIRE(FindControlCodeToConvert("Allowed codes: \t\r\n") == std::string::npos); + REQUIRE(FindControlCodeToConvert("\x1bSkipped code", 1) == std::string::npos); + + REQUIRE(FindControlCodeToConvert("\x1bUnskipped code") == 0); + REQUIRE(FindControlCodeToConvert("Escape code: \x1b") == 13); + + std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv }; + for (size_t i = 0; i < allCodes.length(); ++i) + { + REQUIRE(FindControlCodeToConvert(allCodes, i) == i); + } +} + +TEST_CASE("ConvertControlCodesToPictures", "[strings]") +{ + REQUIRE(ConvertControlCodesToPictures("No codes") == "No codes"); + REQUIRE(ConvertControlCodesToPictures("Allowed codes: \t\r\n") == "Allowed codes: \t\r\n"); + + REQUIRE(ConvertControlCodesToPictures("\x1b Code First") == ConvertToUTF8(L"\x241b Code First")); + REQUIRE(ConvertControlCodesToPictures("Escape code: \x1b") == ConvertToUTF8(L"Escape code: \x241b")); + + std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv }; + std::wstring_view allPictures{ L"\x2400\x2401\x2402\x2403\x2404\x2405\x2406\x2407\x2408\x240b\x240c\x240e\x240f\x2410\x2411\x2412\x2413\x2414\x2415\x2416\x2417\x2418\x2419\x241a\x241b\x241c\x241d\x241e\x241f\x2421"sv }; + + REQUIRE(ConvertControlCodesToPictures(allCodes) == ConvertToUTF8(allPictures)); +} diff --git a/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml b/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml new file mode 100644 index 0000000000..7dee1859d0 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml @@ -0,0 +1 @@ +key: "This is the ESCAPE control code: \x1b" diff --git a/src/AppInstallerCLITests/Yaml.cpp b/src/AppInstallerCLITests/Yaml.cpp new file mode 100644 index 0000000000..47edbb8e16 --- /dev/null +++ b/src/AppInstallerCLITests/Yaml.cpp @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include + +using namespace TestCommon; +using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; + + +TEST_CASE("YamlParserTypes", "[YAML]") +{ + auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); + + auto intUnquoted = document["IntegerUnquoted"]; + CHECK(intUnquoted.GetTagType() == Node::TagType::Int); + + auto intSingleQuoted = document["IntegerSingleQuoted"]; + CHECK(intSingleQuoted.GetTagType() == Node::TagType::Str); + + auto intDoubleQuoted = document["IntegerDoubleQuoted"]; + CHECK(intDoubleQuoted.GetTagType() == Node::TagType::Str); + + auto boolTrue = document["BooleanTrue"]; + CHECK(boolTrue.GetTagType() == Node::TagType::Bool); + + auto strTrue = document["StringTrue"]; + CHECK(strTrue.GetTagType() == Node::TagType::Str); + + auto boolFalse = document["BooleanFalse"]; + CHECK(boolFalse.GetTagType() == Node::TagType::Bool); + + auto strFalse = document["StringFalse"]; + CHECK(strFalse.GetTagType() == Node::TagType::Str); + + auto localTag = document["LocalTag"]; + CHECK(localTag.GetTagType() == Node::TagType::Unknown); +} + +TEST_CASE("YamlMergeMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2); + + REQUIRE(5 == mergeNode.size()); +} + +TEST_CASE("YamlMergeMappingNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2, true); + + REQUIRE(4 == mergeNode.size()); +} + +TEST_CASE("YamlMergeSequenceNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Bounty"); + REQUIRE(5 == document["StrawHats"].size()); +} + +TEST_CASE("YamlMergeSequenceNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Name", true); + REQUIRE(4 == document["StrawHats"].size()); + + auto luffy = std::find_if( + document["StrawHats"].Sequence().begin(), + document["StrawHats"].Sequence().end(), + [](auto const& n) { return n["Name"].as() == "Monkey D Luffy"; }); + REQUIRE(luffy != document["StrawHats"].Sequence().end()); + + // From original node + REQUIRE((*luffy)["Bounty"].as() == "3,000,000,000"); + + // From merged node + REQUIRE((*luffy)["Fruit"].as() == "Gomu Gomu no Mi"); +} + +TEST_CASE("YamlMergeNode_MergeSequenceNoKey", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE_THROWS_HR(document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Power"), APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); +} + +TEST_CASE("YamlMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} + +TEST_CASE("YamlMappingNode_const", "[YAML]") +{ + const auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} + +TEST_CASE("YamlContainsEscapeControlCode", "[YAML]") +{ + REQUIRE_THROWS_HR(Load(TestDataFile("ContainsEscapeControlCode.yaml")), APPINSTALLER_CLI_ERROR_LIBYAML_ERROR); +} diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 4efc06cf7a..3938d20724 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -1789,162 +1789,3 @@ TEST_CASE("ShadowManifest_NotVerifiedPublisher", "[ShadowManifest]") TempFile mergedManifestFile{ "merged.yaml" }; REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("Field usage requires verified publishers. [Icons]")); } - -TEST_CASE("YamlParserTypes", "[YAML]") -{ - auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); - - auto intUnquoted = document["IntegerUnquoted"]; - CHECK(intUnquoted.GetTagType() == Node::TagType::Int); - - auto intSingleQuoted = document["IntegerSingleQuoted"]; - CHECK(intSingleQuoted.GetTagType() == Node::TagType::Str); - - auto intDoubleQuoted = document["IntegerDoubleQuoted"]; - CHECK(intDoubleQuoted.GetTagType() == Node::TagType::Str); - - auto boolTrue = document["BooleanTrue"]; - CHECK(boolTrue.GetTagType() == Node::TagType::Bool); - - auto strTrue = document["StringTrue"]; - CHECK(strTrue.GetTagType() == Node::TagType::Str); - - auto boolFalse = document["BooleanFalse"]; - CHECK(boolFalse.GetTagType() == Node::TagType::Bool); - - auto strFalse = document["StringFalse"]; - CHECK(strFalse.GetTagType() == Node::TagType::Str); - - auto localTag = document["LocalTag"]; - CHECK(localTag.GetTagType() == Node::TagType::Unknown); -} - -TEST_CASE("YamlMergeMappingNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto mergeNode = document["MergeNode"]; - auto mergeNode2 = document["MergeNode2"]; - - REQUIRE(3 == mergeNode.size()); - REQUIRE(2 == mergeNode2.size()); - - mergeNode.MergeMappingNode(mergeNode2); - - REQUIRE(5 == mergeNode.size()); -} - -TEST_CASE("YamlMergeMappingNode_CaseInsensitive", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto mergeNode = document["MergeNode"]; - auto mergeNode2 = document["MergeNode2"]; - - REQUIRE(3 == mergeNode.size()); - REQUIRE(2 == mergeNode2.size()); - - mergeNode.MergeMappingNode(mergeNode2, true); - - REQUIRE(4 == mergeNode.size()); -} - -TEST_CASE("YamlMergeSequenceNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE(3 == document["StrawHats"].size()); - REQUIRE(2 == document2["StrawHats"].size()); - - // Internally will call MergeMappingNode. - document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Bounty"); - REQUIRE(5 == document["StrawHats"].size()); -} - -TEST_CASE("YamlMergeSequenceNode_CaseInsensitive", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE(3 == document["StrawHats"].size()); - REQUIRE(2 == document2["StrawHats"].size()); - - // Internally will call MergeMappingNode. - document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Name", true); - REQUIRE(4 == document["StrawHats"].size()); - - auto luffy = std::find_if( - document["StrawHats"].Sequence().begin(), - document["StrawHats"].Sequence().end(), - [](auto const& n) { return n["Name"].as() == "Monkey D Luffy"; }); - REQUIRE(luffy != document["StrawHats"].Sequence().end()); - - // From original node - REQUIRE((*luffy)["Bounty"].as() == "3,000,000,000"); - - // From merged node - REQUIRE((*luffy)["Fruit"].as() == "Gomu Gomu no Mi"); -} - -TEST_CASE("YamlMergeNode_MergeSequenceNoKey", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE_THROWS_HR(document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Power"), APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); -} - -TEST_CASE("YamlMappingNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto node = document["key"]; - REQUIRE(node.as() == "value"); - - auto node2 = document.GetChildNode("KEY"); - REQUIRE(node2.as() == "value"); - - auto node3 = document.GetChildNode("key"); - REQUIRE(node3.as() == "value"); - - auto node4 = document.GetChildNode("kEy"); - REQUIRE(node4.as() == "value"); - - auto node5 = document.GetChildNode("fake"); - REQUIRE(node5.IsNull()); - - auto node6 = document["repeatedkey"]; - REQUIRE(node6.as() == "repeated value"); - REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - - REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); -} - -TEST_CASE("YamlMappingNode_const", "[YAML]") -{ - const auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto node = document["key"]; - REQUIRE(node.as() == "value"); - - auto node2 = document.GetChildNode("KEY"); - REQUIRE(node2.as() == "value"); - - auto node3 = document.GetChildNode("key"); - REQUIRE(node3.as() == "value"); - - auto node4 = document.GetChildNode("kEy"); - REQUIRE(node4.as() == "value"); - - auto node5 = document.GetChildNode("fake"); - REQUIRE(node5.IsNull()); - - auto node6 = document["repeatedkey"]; - REQUIRE(node6.as() == "repeated value"); - REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - - REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); -} diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 2c7398a852..daa3a14b82 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -49,9 +49,11 @@ namespace AppInstaller::Settings case ExperimentalFeature::Feature::Proxy: return userSettings.Get(); case ExperimentalFeature::Feature::ConfigureSelfElevation: - return userSettings.Get(); + return userSettings.Get(); case ExperimentalFeature::Feature::StoreDownload: return userSettings.Get(); + case ExperimentalFeature::Feature::ConfigureExport: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -92,7 +94,9 @@ namespace AppInstaller::Settings case Feature::ConfigureSelfElevation: return ExperimentalFeature{ "Configure Self Elevation", "configureSelfElevate", "https://aka.ms/winget-settings", Feature::ConfigureSelfElevation }; case Feature::StoreDownload: - return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + case Feature::ConfigureExport: + return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index ab8850cd08..b2a55e4ef0 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,8 +27,9 @@ namespace AppInstaller::Settings Configuration03 = 0x4, Proxy = 0x8, SideBySide = 0x10, - ConfigureSelfElevation = 0x20, + ConfigureSelfElevation = 0x20, StoreDownload = 0x40, + ConfigureExport = 0x80, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index e8b06fedce..e3d3935ffd 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFProxy, EFConfigureSelfElevation, EFStoreDownload, + EFConfigureExport, // Telemetry TelemetryDisable, // Install behavior @@ -159,6 +160,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFProxy, bool, bool, false, ".experimentalFeatures.proxy"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureSelfElevation, bool, bool, false, ".experimentalFeatures.configureSelfElevate"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFStoreDownload, bool, bool, false, ".experimentalFeatures.storeDownload"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 0851745014..59c00be486 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -265,6 +265,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFProxy) WINGET_VALIDATE_PASS_THROUGH(EFConfigureSelfElevation) WINGET_VALIDATE_PASS_THROUGH(EFStoreDownload) + WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 11ae60ecff..a34ddab666 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -10,7 +10,7 @@ namespace AppInstaller::Repository { using namespace std::string_view_literals; - namespace + namespace anon { Utility::VersionAndChannel GetVACFromVersion(IPackageVersion* packageVersion) { @@ -1365,6 +1365,8 @@ namespace AppInstaller::Repository } } + using namespace anon; + CompositeSource::CompositeSource(std::string identifier) { m_details.Identifier = std::move(identifier); diff --git a/src/AppInstallerSharedLib/AppInstallerLogging.cpp b/src/AppInstallerSharedLib/AppInstallerLogging.cpp index 4e2054ae71..295304febf 100644 --- a/src/AppInstallerSharedLib/AppInstallerLogging.cpp +++ b/src/AppInstallerSharedLib/AppInstallerLogging.cpp @@ -20,6 +20,7 @@ namespace AppInstaller::Logging case Channel::Core: return "CORE"; case Channel::Test: return "TEST"; case Channel::Config: return "CONF"; + case Channel::Workflow: return "WORK"; default: return "NONE"; } } @@ -60,6 +61,10 @@ namespace AppInstaller::Logging { return Channel::Config; } + else if (lowerChannel == "workflow") + { + return Channel::Workflow; + } else if (lowerChannel == "default" || lowerChannel == "defaults") { return Channel::Defaults; diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index bf6e7075be..df1cf35496 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -762,9 +762,11 @@ namespace AppInstaller::Utility // If not, round up to the next line count (by rounding down through integer division after subtracting 1 + 1). size_t currentLineActualLineCount = (currentLineWidth ? (currentLineWidth - 1) / lineWidth : 0) + 1; - // The current line may be too big to be the last line. + // The current line may be too big to be the last line, or it may be just the right size but we will end up trimming + // additional lines. In either case, append an ellipsis to indicate that we trimmed the value. size_t availableLines = maximum - totalLines; - if (currentLineActualLineCount > availableLines) + if (currentLineActualLineCount > availableLines || + (currentLineActualLineCount == availableLines && currentLine != lines.size() - 1)) { size_t actualWidth = 0; std::string trimmedLine = UTF8TrimRightToColumnWidth(lines[currentLine], (availableLines * lineWidth) - 1, actualWidth); @@ -827,7 +829,8 @@ namespace AppInstaller::Utility return result; } - LocIndString Join(LocIndView separator, const std::vector& vector) + template + static std::string JoinInternal(std::string_view separator, const std::vector& vector) { auto vectorSize = vector.size(); if (vectorSize == 0) @@ -841,7 +844,17 @@ namespace AppInstaller::Utility { ssJoin << separator << vector[i]; } - return LocIndString{ ssJoin.str() }; + return ssJoin.str(); + } + + LocIndString Join(LocIndView separator, const std::vector& vector) + { + return LocIndString{ JoinInternal(separator, vector) }; + } + + std::string Join(std::string_view separator, const std::vector& vector) + { + return JoinInternal(separator, vector); } std::vector Split(const std::string& input, char separator, bool trim) @@ -909,4 +922,74 @@ namespace AppInstaller::Utility return false; } } + + size_t FindControlCodeToConvert(std::string_view input, size_t offset) + { + size_t nextControl = offset; + while (nextControl < input.size()) + { + char currentChar = input[nextControl]; + + // Convert all low controls except tab, line feed and carriage return + if (currentChar >= 0 && currentChar < 0x20 && + currentChar != '\t' && + currentChar != '\n' && + currentChar != '\r') + { + break; + } + + // Convert the Delete control + if (currentChar == 0x7F) + { + break; + } + + ++nextControl; + } + + return nextControl < input.size() ? nextControl : std::string::npos; + } + + std::string ConvertControlCodesToPictures(std::string_view input) + { + std::string result; + size_t pos = 0; + + while (pos < input.size()) + { + size_t nextControl = FindControlCodeToConvert(input, pos); + + if (nextControl == std::string::npos) + { + // No more control codes found + result += input.substr(pos); + break; + } + else + { + result += input.substr(pos, nextControl - pos); + + char currentChar = input[nextControl]; + + if (currentChar >= 0 && currentChar < 0x20) + { + // ASCII 0x00 - 0x1F => UTF-8 0x2400 - 0x241F + // Then manually converted to UTF-8 since only the last character is affected + result += '\xE2'; + result += '\x90'; + result += ('\x80' + currentChar); + } + else if (currentChar == 0x7F) + { + // UTF-8 for control picture of DELETE + result += "\xE2\x90\xA1"; + } + + pos = nextControl + 1; + } + } + + return result; + } } diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index 17c678cd32..6b207e5629 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -267,6 +267,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_MISSING_FIELD, "The configuration is missing a field."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_FAILED, "Some of the configuration units failed while testing their state."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_NOT_RUN, "Configuration state was not tested."), + WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_GET_FAILED, "The configuration unit failed getting its properties."), // Configuration Processor Errors WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED, "The configuration unit was not installed."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 0114deb175..47b1d59d39 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -202,6 +202,7 @@ #define WINGET_CONFIG_ERROR_MISSING_FIELD ((HRESULT)0x8A15C00E) #define WINGET_CONFIG_ERROR_TEST_FAILED ((HRESULT)0x8A15C00F) #define WINGET_CONFIG_ERROR_TEST_NOT_RUN ((HRESULT)0x8A15C010) +#define WINGET_CONFIG_ERROR_GET_FAILED ((HRESULT)0x8A15C011) // Configuration Processor Errors #define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h index 11cfefbab0..9b958342c1 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h @@ -89,8 +89,20 @@ namespace AppInstaller static constexpr inline size_t Index(Enum e) { return static_cast(e) + 1; } }; + // An action that can be taken on an EnumBasedVariantMap. + enum class EnumBasedVariantMapAction + { + Add, + Contains, + Get, + }; + + // A callback function that can be used for logging map actions. + template + using EnumBasedVariantMapActionCallback = void (*)(const void* map, Enum value, EnumBasedVariantMapAction action); + // Provides a map of the Enum to the mapped types. - template typename Mapping> + template typename Mapping, EnumBasedVariantMapActionCallback Callback = nullptr> struct EnumBasedVariantMap { using Variant = EnumBasedVariant; @@ -103,28 +115,51 @@ namespace AppInstaller template void Add(mapping_t&& v) { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Add); + } m_data[E].emplace(std::move(std::forward>(v))); } template void Add(const mapping_t& v) { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Add); + } m_data[E].emplace(v); } // Return a value indicating whether the given enum is stored in the map. - bool Contains(Enum e) const { return (m_data.find(e) != m_data.end()); } + bool Contains(Enum e) const + { + if constexpr (Callback) + { + Callback(this, e, EnumBasedVariantMapAction::Contains); + } + return (m_data.find(e) != m_data.end()); + } // Gets the value. template mapping_t& Get() { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Get); + } return std::get(GetVariant(E)); } template const mapping_t& Get() const { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Get); + } return std::get(GetVariant(E)); } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h index 22aa749cec..a7ac612abb 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h @@ -56,9 +56,10 @@ namespace AppInstaller::Logging Core = 0x20, Test = 0x40, Config = 0x80, + Workflow = 0x100, None = 0, All = 0xFFFFFFFF, - Defaults = All & ~SQL, + Defaults = All & ~(SQL | Workflow), }; DEFINE_ENUM_FLAG_OPERATORS(Channel); diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 83d78477e0..b2386801fb 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -254,6 +254,9 @@ namespace AppInstaller::Utility // Join a string vector using the provided separator. LocIndString Join(LocIndView separator, const std::vector& vector); + // Join a string vector using the provided separator. + std::string Join(std::string_view separator, const std::vector& vector); + // Splits the string using the provided separator. Entries can also be trimmed. std::vector Split(const std::string& input, char separator, bool trim = false); @@ -278,4 +281,12 @@ namespace AppInstaller::Utility // Converts the input string to a DWORD value using std::stoul and returns a boolean value based on the resulting DWORD value. bool IsDwordFlagSet(const std::string& value); + + // Finds the next control code index that would be replaced. + // Returns std::string::npos if not found. + size_t FindControlCodeToConvert(std::string_view input, size_t offset = 0); + + // Converts most control codes in the input to their corresponding control picture in the output. + // Exempts tab, line feed, and carriage return from being replaced. + std::string ConvertControlCodesToPictures(std::string_view input); } diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h index 83836a342b..3774941df5 100644 --- a/src/AppInstallerSharedLib/Public/winget/Yaml.h +++ b/src/AppInstallerSharedLib/Public/winget/Yaml.h @@ -38,7 +38,8 @@ namespace AppInstaller::YAML Parser, Composer, Writer, - Emitter + Emitter, + Policy, }; // Should only be used for Memory. diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp index e2596629db..4125c31059 100644 --- a/src/AppInstallerSharedLib/Yaml.cpp +++ b/src/AppInstallerSharedLib/Yaml.cpp @@ -45,6 +45,8 @@ namespace AppInstaller::YAML return "Writer"sv; case Exception::Type::Emitter: return "Emitter"sv; + case Exception::Type::Policy: + return "Policy"sv; } return "Unknown"sv; diff --git a/src/AppInstallerSharedLib/YamlWrapper.cpp b/src/AppInstallerSharedLib/YamlWrapper.cpp index a367ab942c..812082e3f0 100644 --- a/src/AppInstallerSharedLib/YamlWrapper.cpp +++ b/src/AppInstallerSharedLib/YamlWrapper.cpp @@ -53,26 +53,36 @@ namespace AppInstaller::YAML::Wrapper THROW_HR(E_UNEXPECTED); } - std::string ConvertYamlString(yaml_char_t* string, size_t length = std::string::npos) + Mark ConvertMark(const yaml_mark_t& mark) { + return { mark.line + 1, mark.column + 1 }; + } + + std::string ConvertYamlString(yaml_char_t* string, const yaml_mark_t& mark, size_t length = std::string::npos) + { + std::string_view resultView; + if (length == std::string::npos) { - return { reinterpret_cast(string) }; + resultView = { reinterpret_cast(string) }; } else { - return { reinterpret_cast(string), length }; + resultView = { reinterpret_cast(string), length }; } - } - std::string ConvertScalarToString(yaml_node_t* node) - { - return ConvertYamlString(node->data.scalar.value, node->data.scalar.length); + size_t invalidCharacter = Utility::FindControlCodeToConvert(resultView); + if (invalidCharacter != std::string::npos) + { + THROW_EXCEPTION(Exception(Exception::Type::Policy, "unsupported control character", ConvertMark(mark))); + } + + return std::string{ resultView }; } - Mark ConvertMark(const yaml_mark_t& mark) + std::string ConvertScalarToString(yaml_node_t* node, const yaml_mark_t& mark) { - return { mark.line + 1, mark.column + 1 }; + return ConvertYamlString(node->data.scalar.value, mark, node->data.scalar.length); } } @@ -115,7 +125,7 @@ namespace AppInstaller::YAML::Wrapper return {}; } - Node result(ConvertNodeType(root->type), ConvertYamlString(root->tag), ConvertMark(root->start_mark)); + Node result(ConvertNodeType(root->type), ConvertYamlString(root->tag, root->start_mark), ConvertMark(root->start_mark)); struct StackItem { @@ -142,7 +152,7 @@ namespace AppInstaller::YAML::Wrapper break; case YAML_SCALAR_NODE: stackItem.node->SetScalar( - ConvertScalarToString(stackItem.yamlNode), + ConvertScalarToString(stackItem.yamlNode, stackItem.yamlNode->start_mark), stackItem.yamlNode->data.scalar.style == YAML_SINGLE_QUOTED_SCALAR_STYLE || stackItem.yamlNode->data.scalar.style == YAML_DOUBLE_QUOTED_SCALAR_STYLE); pop = true; @@ -153,7 +163,7 @@ namespace AppInstaller::YAML::Wrapper if (child < stackItem.yamlNode->data.sequence.items.top) { yaml_node_t* childYamlNode = GetNode(*child); - Node& childNode = stackItem.node->AddSequenceNode(ConvertNodeType(childYamlNode->type), ConvertYamlString(childYamlNode->tag), ConvertMark(childYamlNode->start_mark)); + Node& childNode = stackItem.node->AddSequenceNode(ConvertNodeType(childYamlNode->type), ConvertYamlString(childYamlNode->tag, childYamlNode->start_mark), ConvertMark(childYamlNode->start_mark)); resultStack.emplace(childYamlNode, &childNode); } else @@ -171,12 +181,12 @@ namespace AppInstaller::YAML::Wrapper yaml_node_t* keyYamlNode = GetNode(child->key); THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_INVALID_MAPPING_KEY, keyYamlNode->type != YAML_SCALAR_NODE); - Node keyNode(ConvertNodeType(keyYamlNode->type), ConvertYamlString(keyYamlNode->tag), ConvertMark(keyYamlNode->start_mark)); - keyNode.SetScalar(ConvertScalarToString(keyYamlNode)); + Node keyNode(ConvertNodeType(keyYamlNode->type), ConvertYamlString(keyYamlNode->tag, keyYamlNode->start_mark), ConvertMark(keyYamlNode->start_mark)); + keyNode.SetScalar(ConvertScalarToString(keyYamlNode, keyYamlNode->start_mark)); yaml_node_t* valueYamlNode = GetNode(child->value); - Node& childNode = stackItem.node->AddMappingNode(std::move(keyNode), ConvertNodeType(valueYamlNode->type), ConvertYamlString(valueYamlNode->tag), ConvertMark(valueYamlNode->start_mark)); + Node& childNode = stackItem.node->AddMappingNode(std::move(keyNode), ConvertNodeType(valueYamlNode->type), ConvertYamlString(valueYamlNode->tag, valueYamlNode->start_mark), ConvertMark(valueYamlNode->start_mark)); resultStack.emplace(valueYamlNode, &childNode); } else diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs index 40ee9560cd..3639d2f71c 100644 --- a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -70,5 +70,10 @@ internal static class ErrorCodes /// The module where the DSC resource is implemented requires admin. /// internal const int WinGetConfigUnitImportModuleAdmin = unchecked((int)0x8A15C111); + + /// + /// The property type of a unit is not supported. + /// + internal const int WinGetConfigUnitUnsupportedType = unchecked((int)0x8A15C112); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs new file mode 100644 index 0000000000..205f3e010a --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// + /// The property type of a unit is not supported. + /// + internal class UnitPropertyUnsupportedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Name. + /// Type. + /// Inner exception. + public UnitPropertyUnsupportedException(string name, Type type, Exception inner) + : base($"Property {name} of type {type.FullName} is not supported.", inner) + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Name = name; + this.Type = type; + } + + /// + /// Initializes a new instance of the class. + /// + /// Type. + public UnitPropertyUnsupportedException(Type type) + : base($"Type {type.FullName} is not supported.") + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Type = type; + } + + /// + /// Gets the name. + /// + public string? Name { get; } + + /// + /// Gets the type. + /// + public Type Type { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs new file mode 100644 index 0000000000..b11b4e2005 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Windows.Foundation.Collections; + + /// + /// Extensions for Hashtable. + /// + internal static class HashtableExtensions + { + /// + /// Convert a hashtable to a value set. + /// + /// hashtable. + /// Value set. + public static ValueSet ToValueSet(this Hashtable hashtable) + { + var valueSet = new ValueSet(); + + foreach (DictionaryEntry entry in hashtable) + { + if (entry.Key is string key) + { + if (entry.Value is null) + { + valueSet.Add(key, null); + } + else + { + var value = TypeHelpers.GetCompatibleValueSetValueOfProperty(entry.Value.GetType(), entry.Value); + if (value != null) + { + valueSet.Add(key, value); + } + } + } + else + { + throw new UnitPropertyUnsupportedException(entry.Key.GetType()); + } + } + + return valueSet; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs index abb32ce1cc..e9ad218a3a 100644 --- a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs +++ b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,8 +6,12 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { + using System; + using System.Collections; using System.Collections.Generic; using System.Reflection; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; using Windows.Foundation.Collections; /// @@ -74,18 +78,78 @@ public static ValueSet GetAllPropertiesValues(object obj) var result = new ValueSet(); foreach (PropertyInfo property in obj.GetType().GetProperties()) { - // Specialize here. - if (property.PropertyType.IsEnum) + var key = property.Name; + var value = GetCompatibleValueSetValueOfProperty(property.PropertyType, property.GetValue(obj)); + result.Add(key, value); + } + + return result; + } + + /// + /// Gets a compatible type for a ValueSet value. + /// + /// Type. + /// Value. + /// Value converted to a compatible type. + public static object? GetCompatibleValueSetValueOfProperty(Type type, object? value) + { + if (value == null) + { + return null; + } + + // Specialize here. + if (type.IsEnum) + { + return value.ToString(); + } + else if (type == typeof(Hashtable)) + { + Hashtable hashtable = (Hashtable)value; + return hashtable.ToValueSet(); + } + else if (type.IsArray) + { + var valueSetArray = new ValueSet(); + int index = 0; + foreach (object arrayObj in (Array)value) + { + var arrayValue = GetCompatibleValueSetValueOfProperty(arrayObj.GetType(), arrayObj); + if (arrayValue != null) + { + valueSetArray.Add(index.ToString(), arrayValue); + index++; + } + } + + if (valueSetArray.Count > 0) + { + valueSetArray.Add("treatAsArray", true); + } + + return valueSetArray; + } + else if (type == typeof(string)) + { + // Ignore empty strings. + string propertyString = (string)value; + if (!string.IsNullOrEmpty(propertyString)) { - result.Add(property.Name, property.GetValue(obj)?.ToString()); + return propertyString; } else { - result.Add(property.Name, property.GetValue(obj)); + return null; } } + else if (type.IsValueType) + { + return value; + } - return result; + // This might be too restrictive but anything else is going to be some object that we don't support anyway. + throw new UnitPropertyUnsupportedException(value.GetType()); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs index e2d3e1fadf..c512fa32b1 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs @@ -31,6 +31,7 @@ internal static class Errors public static readonly int WINGET_CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public static readonly int WINGET_CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public static readonly int WINGET_CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public static readonly int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); // Configuration Processor Errors public static readonly int WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs new file mode 100644 index 0000000000..2c5d21d4fc --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// + /// Hashtable extension tests. + /// + [Collection("UnitTestCollection")] + public class HashtableExtensionsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// + /// Initializes a new instance of the class. + /// + /// Unit test fixture. + /// Log helper. + public HashtableExtensionsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// + /// Tests ToValueSet with simple types. + /// + [Fact] + public void ToValueSet_Test() + { + var ht = new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)valueSet["key1"]); + + Assert.True(valueSet.ContainsKey("key2")); + Assert.Equal(2, (int)valueSet["key2"]); + + Assert.True(valueSet.ContainsKey("key3")); + Assert.True((bool)valueSet["key3"]); + } + + /// + /// Test for inner hashtables. + /// + [Fact] + public void ToValueSet_InnerHashtable() + { + var ht = new Hashtable() + { + { "hashtableKey", new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("hashtableKey")); + var resultValueSet = (ValueSet)valueSet["hashtableKey"]; + + Assert.True(resultValueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)resultValueSet["key1"]); + + Assert.True(resultValueSet.ContainsKey("key2")); + Assert.Equal(2, (int)resultValueSet["key2"]); + + Assert.True(resultValueSet.ContainsKey("key3")); + Assert.True((bool)resultValueSet["key3"]); + } + + /// + /// Test for inner arrays. + /// + [Fact] + public void ToValueSet_InnerArray() + { + var ht = new Hashtable() + { + { + "arrayKey", new string[] + { + "s1", + "s2", + "s3", + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("arrayKey")); + var resultValueSet = (ValueSet)valueSet["arrayKey"]; + Assert.True(resultValueSet.ContainsKey("treatAsArray")); + Assert.Equal(4, resultValueSet.Count); + } + + /// + /// Test when a key is not a string. + /// + [Fact] + public void ToValueSet_KeyNotString() + { + var ht = new Hashtable() + { + { 1, "value" }, + }; + + Assert.Throws(() => ht.ToValueSet()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs index db34cb2f05..65e6e00803 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -482,18 +482,18 @@ public void TestSet_Serialize_0_2() properties: configurationVersion: 0.2 assertions: - - resource: FakeModule + - resource: FakeModule/FakeResource id: TestId directives: description: FakeDescription allowPrerelease: true - SecurityContext: elevated + securityContext: elevated settings: TestString: Hello TestBool: false TestInt: 1234 resources: - - resource: FakeModule2 + - resource: FakeModule2/FakeResource2 id: TestId2 dependsOn: - TestId @@ -501,7 +501,7 @@ public void TestSet_Serialize_0_2() - dependency3 directives: description: FakeDescription2 - SecurityContext: elevated + securityContext: elevated settings: TestString: Bye TestBool: true @@ -526,17 +526,17 @@ public void TestSet_Serialize_0_2() Assert.Equal("0.2", set.SchemaVersion); Assert.Equal(2, set.Units.Count); - Assert.Equal("FakeModule", set.Units[0].Type); + Assert.Equal("FakeResource", set.Units[0].Type); Assert.Equal(ConfigurationUnitIntent.Assert, set.Units[0].Intent); Assert.Equal("TestId", set.Units[0].Identifier); - this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("SecurityContext", "elevated")); + this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated"), new ("module", "FakeModule")); this.VerifyValueSet(set.Units[0].Settings, new ("TestString", "Hello"), new ("TestBool", false), new ("TestInt", 1234)); - Assert.Equal("FakeModule2", set.Units[1].Type); + Assert.Equal("FakeResource2", set.Units[1].Type); Assert.Equal(ConfigurationUnitIntent.Apply, set.Units[1].Intent); Assert.Equal("TestId2", set.Units[1].Identifier); this.VerifyStringArray(set.Units[1].Dependencies, "TestId", "dependency2", "dependency3"); - this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("SecurityContext", "elevated")); + this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated"), new ("module", "FakeModule2")); ValueSet mapping = new ValueSet(); mapping.Add("Key", "TestValue"); @@ -757,7 +757,7 @@ private void VerifyValueSet(ValueSet values, params KeyValuePair foreach (var expectation in expected) { - Assert.True(values.ContainsKey(expectation.Key)); + Assert.True(values.ContainsKey(expectation.Key), $"Not Found {expectation.Key}"); object value = values[expectation.Key]; switch (expectation.Value) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs index 91345b4105..c18a616b72 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,6 +6,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { + using System.Collections; using System.Collections.Generic; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.UnitTests.Fixtures; @@ -136,5 +137,75 @@ public void GetAllPropertiesValuesTests() Assert.True(set.TryGetValue("Property3", out object v3)); Assert.Equal(e.ToString(), v3); } + + /// + /// Verifies when a property is a Hashtable. It must be converted to a ValueSet. + /// + [Fact] + public void GetAllPropertiesValuesTest_Hashtable() + { + string k1 = "key1"; + string k2 = "key2"; + int v1 = 7; + string v2 = "value2"; + dynamic obj = new + { + Property1 = new Hashtable + { + { k1, v1 }, + { k2, v2 }, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(2, valueSetResult.Count); + Assert.True(valueSetResult.ContainsKey(k1)); + Assert.Equal(v1, (int)valueSetResult[k1]); + Assert.True(valueSetResult.ContainsKey(k2)); + Assert.Equal(v2, (string)valueSetResult[k2]); + } + + /// + /// Verifies when a property is an array. It must generate a ValueSet + /// where the keys are the index and a key treatAsArray means the value + /// must be treated an array. + /// + [Fact] + public void GetAllPropertiesValuesTest_Array() + { + dynamic obj = new + { + Property1 = new int[] + { + 1, + 2, + 3, + 4, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(5, valueSetResult.Count); + + Assert.True(valueSetResult.ContainsKey("treatAsArray")); + Assert.True(valueSetResult.ContainsKey("0")); + Assert.True(valueSetResult.ContainsKey("1")); + Assert.True(valueSetResult.ContainsKey("2")); + Assert.True(valueSetResult.ContainsKey("3")); + } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp index 2107fdef17..215ac30e52 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -15,6 +15,11 @@ using namespace winrt::Windows::Foundation; namespace winrt::Microsoft::Management::Configuration::implementation { + namespace anon + { + static constexpr std::string_view s_nullValue = "null"; + } + std::unique_ptr ConfigurationSetSerializer::CreateSerializer(hstring version) { // Create the parser based on the version selected @@ -46,13 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation for (const auto& [key, value] : valueSet) { - std::string keyName = winrt::to_string(key); - const auto& currentValueSet = value.try_as(); + if (value != nullptr) + { + std::string keyName = winrt::to_string(key); + emitter << Key << keyName << Value; + WriteYamlValue(emitter, value); + } + } + + emitter << EndMap; + } + + void ConfigurationSetSerializer::WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value) + { + if (value == nullptr) + { + emitter << anon::s_nullValue; + } + else + { + const auto& currentValueSet = value.try_as(); if (currentValueSet) { - emitter << AppInstaller::YAML::Key << keyName; - WriteYamlValueSet(emitter, currentValueSet); + if (currentValueSet.HasKey(L"treatAsArray")) + { + WriteYamlValueSetAsArray(emitter, currentValueSet); + } + else + { + WriteYamlValueSet(emitter, currentValueSet); + } } else { @@ -61,15 +90,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (type == PropertyType::Boolean) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetBoolean(); + emitter << property.GetBoolean(); } else if (type == PropertyType::String) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << AppInstaller::Utility::ConvertToUTF8(property.GetString()); + emitter << AppInstaller::Utility::ConvertToUTF8(property.GetString()); } else if (type == PropertyType::Int64) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetInt64(); + emitter << property.GetInt64(); } else { @@ -77,8 +106,35 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } } + } - emitter << EndMap; + void ConfigurationSetSerializer::WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray) + { + std::vector> arrayValues; + for (const auto& arrayValue : valueSetArray) + { + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } + } + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + + emitter << BeginSeq; + + for (const auto& arrayValue : arrayValues) + { + WriteYamlValue(emitter, arrayValue.second); + } + + emitter << EndSeq; } void ConfigurationSetSerializer::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units) @@ -89,7 +145,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { // Resource emitter << BeginMap; - emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(unit.Type()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(GetResourceName(unit)); // Id if (!unit.Identifier().empty()) @@ -112,9 +168,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } // Directives - const auto& metadata = unit.Metadata(); - emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); - WriteYamlValueSet(emitter, metadata); + WriteResourceDirectives(emitter, unit); // Settings const auto& settings = unit.Settings(); @@ -126,4 +180,21 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndSeq; } + + winrt::hstring ConfigurationSetSerializer::GetResourceName(const ConfigurationUnit& unit) + { + return unit.Type(); + } + + void ConfigurationSetSerializer::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); + } + + winrt::hstring ConfigurationSetSerializer::GetSchemaVersionComment(winrt::hstring version) + { + return winrt::to_hstring(L"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/") + version; + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h index bf522172c4..3c7e94223f 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -25,8 +25,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation protected: ConfigurationSetSerializer() = default; + void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet); + void WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value); + void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); + winrt::hstring GetSchemaVersionComment(winrt::hstring version); - void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units); + virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit) = 0; + virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) = 0; }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp index 5631e1d2e3..1d7293f88e 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp @@ -9,6 +9,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using namespace AppInstaller::YAML; + using namespace winrt::Windows::Foundation; hstring ConfigurationSetSerializer_0_2::Serialize(ConfigurationSet* configurationSet) { @@ -50,6 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndMap; emitter << EndMap; - return winrt::to_hstring(emitter.str()); + return GetSchemaVersionComment(configurationSet->SchemaVersion()) + winrt::to_hstring(L"\n") + winrt::to_hstring(emitter.str()); + } + + winrt::hstring ConfigurationSetSerializer_0_2::GetResourceName(const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + auto object = metadata.Lookup(moduleKey); + auto property = object.try_as(); + if (property && property.Type() == PropertyType::String) + { + return property.GetString() + '/' + unit.Type(); + } + } + + return unit.Type(); + } + + void ConfigurationSetSerializer_0_2::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + auto metadata = unit.Metadata(); + + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + metadata.Remove(moduleKey); + } + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h index c7034215d9..29f2773d00 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h @@ -18,5 +18,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationSetSerializer_0_2& operator=(ConfigurationSetSerializer_0_2&&) = default; hstring Serialize(ConfigurationSet* configurationSet) override; + + protected: + winrt::hstring GetResourceName(const ConfigurationUnit& unit) override; + void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) override; }; } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters index ed58521881..5c2caffcba 100644 --- a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters +++ b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -222,11 +222,15 @@ Internals - - Parser + + Parser + + + Parser + diff --git a/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 b/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 new file mode 100644 index 0000000000..2b46e7907e --- /dev/null +++ b/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Creates a local Microsoft.WindowsPackageManager.Utils nuget package and add it to a local nuget feed. +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [ValidateSet("Debug", "Release", "ReleaseStatic")] + [string] + $Configuration, + + [Parameter(Mandatory)] + [string] + $NugetVersion, + + [string] + $BuildRoot = "", + + [string] + $LocalNugetSource = "" +) + +if ($BuildRoot -eq "") +{ + $BuildRoot = "$PSScriptRoot\..\.."; +} + +$local:repoPath = "$PSScriptRoot\..\..\..\" + +# Create all directories and copy files in location expected from the nuspec. +# The paths contains 'release' but it whatever configuration the param is. +$local:nugetWorkingDir = "$PSScriptRoot\NugetFiles" +$local:x64NugetPath = "$nugetWorkingDir\Build.x64release\src\x64\Release\WinGetUtil" +$local:x86NugetPath = "$nugetWorkingDir\Build.x86release\src\x86\Release\WinGetUtil" +$local:manifestsNugetPath = "$nugetWorkingDir\Build.x64release\schemas\JSON\manifests" +$local:interopNugetPath = "$nugetWorkingDir\Build.x64release\src\WinGetUtilInterop\bin\Release\netstandard2.1" +$local:targetsNugetPath = "$nugetWorkingDir\Build.x64release\src\WinGetUtilInterop\build" + +Write-Host "Prepare nuget files" +if (Test-Path $nugetWorkingDir) +{ + Remove-Item $nugetWorkingDir -Recurse +} +New-Item $nugetWorkingDir -ItemType directory | Out-Null +New-Item $x64NugetPath -ItemType directory | Out-Null +New-Item $x86NugetPath -ItemType directory | Out-Null +New-Item $manifestsNugetPath -ItemType directory | Out-Null +New-Item $interopNugetPath -ItemType directory | Out-Null +New-Item $targetsNugetPath -ItemType directory | Out-Null + +function CopyFile([string]$in, [string]$out) +{ + $copyErrors = $null + Copy-Item $in $out -Force -ErrorVariable copyErrors -ErrorAction SilentlyContinue + $copyErrors | ForEach-Object { Write-Warning $_ } +} + +CopyFile "$BuildRoot\x64\$Configuration\WinGetUtil\WinGetUtil.dll" "$x64NugetPath\WinGetUtil.dll" +CopyFile "$BuildRoot\x64\$Configuration\WinGetUtil\WinGetUtil.pdb" "$x64NugetPath\WinGetUtil.pdb" +CopyFile "$BuildRoot\x86\$Configuration\WinGetUtil\WinGetUtil.dll" "$x86NugetPath\WinGetUtil.dll" +CopyFile "$BuildRoot\x86\$Configuration\WinGetUtil\WinGetUtil.pdb" "$x86NugetPath\WinGetUtil.pdb" +CopyFile "$repoPath\src\WinGetUtilInterop\bin\$Configuration\netstandard2.1\WinGetUtilInterop.dll" "$interopNugetPath\WinGetUtilInterop.dll" +CopyFile "$repoPath\src\WinGetUtilInterop\bin\$Configuration\netstandard2.1\WinGetUtilInterop.pdb" "$interopNugetPath\WinGetUtilInterop.pdb" +CopyFile "$repoPath\src\WinGetUtilInterop\build\Microsoft.WindowsPackageManager.Utils.targets" "$targetsNugetPath\Microsoft.WindowsPackageManager.Utils.targets" +CopyFile "$repoPath\WinGetUtil.nuspec" "$nugetWorkingDir\WinGetUtil.nuspec" +Copy-Item "$repoPath\schemas\JSON\manifests" $manifestsNugetPath -Recurse + +# Create nuget +Write-Host "Creating nuget package" +$local:result = nuget pack .\NugetFiles\WinGetUtil.nuspec -Version $NugetVersion -OutputDirectory NugetOut +$local:outFile = $result -match "nupkg" +$outFile = $outFile[0] +$outFile = $outFile.Substring($outFile.IndexOf("'") + 1, $outFile.LastIndexOf("'") - $outFile.IndexOf("'") - 1) +Write-Host "Created $outFile" + +if ($LocalNugetSource -ne "") +{ + Write-Host "Adding $outFile to local nuget feed" + nuget add $outFile -Source $LocalNugetSource +} +