winget-cli

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

commit 2dc4c07f056effe66cf83710b343b33d413ad8f2
parent 0daa420c232950d4308ef3cdf2fd6c75679bad1d
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Mon, 27 Jan 2025 17:55:06 -0800

Add support to export all installed packages in winget configure export (#5156)

The feature is in parity with `winget export` command. Except the
exported file is in winget configuration yaml format. The exported file
is ready to be used with winget configure commands.

New additions:
- When exporting a specific package (existing functionality), in
addition we'll search the source to find the package before export. If
the package is not from well known source, the source is exported as
well. Supports exporting the version like `winget export` too.
- Added `--all` to export all installed packages. We reuse the workfow
in `winget export` to collect packages to export. For non well known
sources, the sources are exported as well. `--all` cannot be used with
other specific package export arguments.

Manually validated and added e2e tests.
Diffstat:
Msrc/AppInstallerCLICore/Argument.cpp | 2++
Msrc/AppInstallerCLICore/ConfigureExportCommand.cpp | 22+++++++++++++++-------
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 4+++-
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 271++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.h | 10++++++++--
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 2+-
Asrc/AppInstallerCLIE2ETests/ConfigureExportCommand.cs | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml | 20++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 17++++++++++++-----
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySource.h | 3+++
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 5+++++
12 files changed, 403 insertions(+), 108 deletions(-)

diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -223,6 +223,8 @@ namespace AppInstaller::CLI return { type, "module"_liv }; case Execution::Args::Type::ConfigurationExportResource: return { type, "resource"_liv }; + case Execution::Args::Type::ConfigurationExportAll: + return { type, "all"_liv, 'r', "recurse"_liv }; case Execution::Args::Type::ConfigurationHistoryItem: return { type, "history"_liv, 'h', ArgTypeCategory::ConfigurationSetChoice, ArgTypeExclusiveSet::ConfigurationSetChoice }; case Execution::Args::Type::ConfigurationHistoryRemove: diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -17,6 +17,10 @@ namespace AppInstaller::CLI Argument{ Execution::Args::Type::ConfigurationExportModule, Resource::String::ConfigureExportModule }, Argument{ Execution::Args::Type::ConfigurationExportResource, Resource::String::ConfigureExportResource }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath }, + Argument{ Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, + Argument{ Execution::Args::Type::IncludeVersions, Resource::String::ExportIncludeVersionsArgumentDescription, ArgumentType::Flag }, + Argument{ Execution::Args::Type::ConfigurationExportAll, Resource::String::ConfigureExportAll, ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } @@ -39,9 +43,10 @@ namespace AppInstaller::CLI { context << VerifyIsFullPackage << + SearchSourceForPackageExport << CreateConfigurationProcessor << CreateOrOpenConfigurationSet << - AddWinGetPackageAndResource << + PopulateConfigurationSetForExport << WriteConfigFile; } @@ -49,16 +54,19 @@ namespace AppInstaller::CLI { Configuration::ValidateCommonArguments(execArgs); - bool validInputArgs = false; - if (execArgs.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource) || - execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + if (!execArgs.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource) && + !execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId) && + !execArgs.Contains(Execution::Args::Type::ConfigurationExportAll)) { - validInputArgs = true; + throw CommandException(Resource::String::ConfigureExportArgumentRequiredError); } - if (!validInputArgs) + if (execArgs.Contains(Execution::Args::Type::ConfigurationExportAll) && + (execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId) || + execArgs.Contains(Execution::Args::Type::ConfigurationExportModule) || + execArgs.Contains(Execution::Args::Type::ConfigurationExportResource))) { - throw CommandException(Resource::String::ConfigureExportArgumentError); + throw CommandException(Resource::String::ConfigureExportArgumentConflictWithAllError); } } } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -135,6 +135,7 @@ namespace AppInstaller::CLI::Execution ConfigurationExportPackageId, ConfigurationExportModule, ConfigurationExportResource, + ConfigurationExportAll, ConfigurationHistoryItem, ConfigurationHistoryRemove, ConfigurationStatusWatch, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -153,7 +153,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningValueTruncated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandShortDescription); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportAll); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentConflictWithAllError); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentRequiredError); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportModule); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "ConfigurationFlow.h" +#include "ImportExportFlow.h" #include "PromptFlow.h" #include "TableOutput.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" @@ -42,13 +43,17 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_Directive_AllowPrerelease = L"allowPrerelease"; constexpr std::wstring_view s_Unit_WinGetPackage = L"WinGetPackage"; + constexpr std::wstring_view s_Unit_WinGetSource = L"WinGetSource"; 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_Setting_WinGetPackage_Id = L"id"; + constexpr std::wstring_view s_Setting_WinGetPackage_Source = L"source"; + constexpr std::wstring_view s_Setting_WinGetPackage_Version = L"version"; - constexpr std::wstring_view s_WinGetSource = L"winget"; + constexpr std::wstring_view s_Setting_WinGetSource_Name = L"name"; + constexpr std::wstring_view s_Setting_WinGetSource_Arg = L"argument"; + constexpr std::wstring_view s_Setting_WinGetSource_Type = L"type"; Logging::Level ConvertLevel(DiagnosticLevel level) { @@ -1064,36 +1069,71 @@ namespace AppInstaller::CLI::Workflow context.Get<Data::ConfigurationContext>().Set(result); } - std::optional<ConfigurationUnit> CreateWinGetUnit(const Execution::Context& context) + ConfigurationUnit CreateWinGetSourceUnit(const PackageCollection::Source& source) { - 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); + std::string sourceUnitId = source.Details.Name + '_' + source.Details.Type; + std::wstring sourceUnitIdWide = Utility::ConvertToUTF16(sourceUnitId); + + ConfigurationUnit unit; + unit.Type(s_Unit_WinGetSource); + unit.Identifier(sourceUnitIdWide); + unit.Intent(ConfigurationUnitIntent::Apply); + + auto description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ sourceUnitId }); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(s_Module_WinGetClient)); + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + ValueSet settings; + settings.Insert(s_Setting_WinGetSource_Name, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Name))); + settings.Insert(s_Setting_WinGetSource_Arg, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Arg))); + settings.Insert(s_Setting_WinGetSource_Type, PropertyValue::CreateString(Utility::ConvertToUTF16(source.Details.Type))); + unit.Settings(settings); - ConfigurationUnit unit; - unit.Type(s_Unit_WinGetPackage); - unit.Identifier(packageIdWide); - unit.Intent(ConfigurationUnitIntent::Apply); + unit.Environment().Context(SecurityContext::Elevated); + + return unit; + } + + ConfigurationUnit CreateWinGetPackageUnit(const PackageCollection::Package& package, const PackageCollection::Source& source, bool includeVersion, const std::optional<ConfigurationUnit>& dependentUnit) + { + std::wstring packageIdWide = Utility::ConvertToUTF16(package.Id); + std::wstring sourceNameWide = Utility::ConvertToUTF16(source.Details.Name); - auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ packageId }); + ConfigurationUnit unit; + unit.Type(s_Unit_WinGetPackage); + unit.Identifier(sourceNameWide + L'_' + packageIdWide); + unit.Intent(ConfigurationUnitIntent::Apply); - 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); + auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ package.Id }); - ValueSet settings; - settings.Insert(s_Setting_Id, PropertyValue::CreateString(packageIdWide)); - settings.Insert(s_Setting_Source, PropertyValue::CreateString(s_WinGetSource)); - unit.Settings(settings); + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(s_Module_WinGetClient)); + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); - return unit; + ValueSet settings; + settings.Insert(s_Setting_WinGetPackage_Id, PropertyValue::CreateString(packageIdWide)); + settings.Insert(s_Setting_WinGetPackage_Source, PropertyValue::CreateString(sourceNameWide)); + if (includeVersion) + { + settings.Insert(s_Setting_WinGetPackage_Version, PropertyValue::CreateString(Utility::ConvertToUTF16(package.VersionAndChannel.GetVersion().ToString()))); } + unit.Settings(settings); - return {}; + // TODO: We may consider setting security environment based on installer elevation requirements? + + // Add dependency if needed. + if (dependentUnit.has_value()) + { + auto dependencies = winrt::single_threaded_vector<winrt::hstring>(); + dependencies.Append(dependentUnit.value().Identifier()); + unit.Dependencies(std::move(dependencies)); + } + + return unit; } GetConfigurationUnitSettingsResult GetUnitSettings(Execution::Context& context, ConfigurationUnit& unit) @@ -1118,84 +1158,76 @@ namespace AppInstaller::CLI::Workflow return getResult; } - std::optional<ConfigurationUnit> CreateConfigurationUnit(Execution::Context& context, const std::optional<ConfigurationUnit> dependentUnit) + ConfigurationUnit CreateConfigurationUnit(Execution::Context& context, std::string_view moduleName, std::string_view resourceName, const std::optional<ConfigurationUnit>& dependentUnit) { - 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); + std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); + std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); - ConfigurationUnit unit; - unit.Type(resourceNameWide); + ConfigurationUnit unit; + unit.Type(resourceNameWide); - ValueSet directives; - directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); - Utility::LocIndString description; - if (dependentUnit.has_value()) - { - description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependentUnit.value().Identifier()) }); - } - else - { - description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); - } + Utility::LocIndString description; + if (dependentUnit.has_value()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependentUnit.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); + 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)) + // 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) { - // 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); + 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()); - } + auto preReleaseResult = GetUnitSettings(context, unit); + if (SUCCEEDED(preReleaseResult.ResultInformation().ResultCode())) + { + isPreRelease = true; + getResult = preReleaseResult; } - - if (!isPreRelease) + else { - OutputUnitRunFailure(context, unit, getResult.ResultInformation()); - THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + AICLI_LOG(Config, Error, << "Failed Get allowing prerelease modules"); + LogFailedGetConfigurationUnitDetails(unit, preReleaseResult.ResultInformation()); } } - unit.Settings(getResult.Settings()); - - // GetUnitSettings will set it to Inform. - unit.Intent(ConfigurationUnitIntent::Apply); - - // Add dependency if needed. - if (dependentUnit.has_value()) + if (!isPreRelease) { - auto dependencies = winrt::single_threaded_vector<winrt::hstring>(); - dependencies.Append(dependentUnit.value().Identifier()); - unit.Dependencies(std::move(dependencies)); + 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); - return unit; + // Add dependency if needed. + if (dependentUnit.has_value()) + { + auto dependencies = winrt::single_threaded_vector<winrt::hstring>(); + dependencies.Append(dependentUnit.value().Identifier()); + unit.Dependencies(std::move(dependencies)); } - return {}; + return unit; } bool HistorySetMatchesInput(const ConfigurationSet& set, const std::string& foldedInput) @@ -1827,20 +1859,75 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::ConfigurationValidationFoundNoIssues << std::endl; } - void AddWinGetPackageAndResource(Execution::Context& context) + void SearchSourceForPackageExport(Execution::Context& context) { - auto wingetUnit = anon::CreateWinGetUnit(context); - auto configUnit = anon::CreateConfigurationUnit(context, wingetUnit); + if (!context.Args.Contains(Args::Type::ConfigurationExportAll) && !context.Args.Contains(Args::Type::ConfigurationExportPackageId)) + { + // No package export needed. + return; + } + + context << + OpenSource() << + OpenCompositeSource(Repository::PredefinedSource::Installed); + + if (context.Args.Contains(Args::Type::ConfigurationExportAll)) + { + context << + SearchSourceForMany << + HandleSearchResultFailures << + EnsureMatchesFromSearchResult(OperationType::Export) << + SelectVersionsToExport; + } + else if (context.Args.Contains(Args::Type::ConfigurationExportPackageId)) + { + context.Args.AddArg(Args::Type::Id, context.Args.GetArg(Args::Type::ConfigurationExportPackageId)); + context << + SearchSourceForSingle << + Workflow::HandleSearchResultFailures << + Workflow::EnsureOneMatchFromSearchResult(OperationType::Export) << + SelectVersionsToExport; + } + } + void PopulateConfigurationSetForExport(Execution::Context& context) + { ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); - if (wingetUnit.has_value()) + + // When exporting single WinGetPackage unit, the WinGetPackage unit can be used as a dependent unit for following configuration unit. + // This is not used in export all scenario. + std::optional<ConfigurationUnit> singlePackageUnit; + + for (const auto& source : context.Get<Execution::Data::PackageCollection>().Sources) { - configContext.Set().Units().Append(wingetUnit.value()); + // Create WinGetSource unit for non well known source. + std::optional<ConfigurationUnit> sourceUnit; + if (!CheckForWellKnownSource(source.Details)) + { + sourceUnit = anon::CreateWinGetSourceUnit(source); + configContext.Set().Units().Append(sourceUnit.value()); + } + + for (const auto& package : source.Packages) + { + auto packageUnit = anon::CreateWinGetPackageUnit(package, source, context.Args.Contains(Args::Type::IncludeVersions), sourceUnit); + configContext.Set().Units().Append(packageUnit); + if (!singlePackageUnit) + { + singlePackageUnit = packageUnit; + } + } } - if (configUnit.has_value()) + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) { - configContext.Set().Units().Append(configUnit.value()); + auto configUnit = anon::CreateConfigurationUnit( + context, + context.Args.GetArg(Args::Type::ConfigurationExportModule), + context.Args.GetArg(Args::Type::ConfigurationExportResource), + singlePackageUnit); + + configContext.Set().Units().Append(configUnit); } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -97,11 +97,17 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ValidateAllGoodMessage(Execution::Context& context); - // Adds a configuration unit with the winget package and/or exports resource given. + // Search source for package(s) to be exported in configuration file. + // Required Args: None + // Inputs: None + // Outputs: PackageCollection + void SearchSourceForPackageExport(Execution::Context& context); + + // Adds configuration unit(s) with the winget package and/or exports resource given to configuration set. // Required Args: None // Inputs: ConfigurationProcessor, ConfigurationSet // Outputs: None - void AddWinGetPackageAndResource(Execution::Context& context); + void PopulateConfigurationSetForExport(Execution::Context& context); // Write the configuration file. // Required Args: OutputFile diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -1105,7 +1105,7 @@ namespace AppInstaller::CLI::Workflow { Logging::Telemetry().LogMultiAppMatch(); - if (m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || m_operationType == OperationType::Repair) + if (m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || m_operationType == OperationType::Repair || m_operationType == OperationType::Export) { context.Reporter.Warn() << Resource::String::MultipleInstalledPackagesFound << std::endl; context << ReportMultiplePackageFoundResult; diff --git a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs @@ -0,0 +1,154 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigureExportCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + using NUnit.Framework.Internal; + + /// <summary> + /// `Configure export` command tests. + /// </summary> + public class ConfigureExportCommand + { + private const string Command = "configure export"; + private const string ShowCommand = "configure show"; + + /// <summary> + /// Set up. + /// </summary> + [OneTimeSetUp] + public void BaseSetup() + { + TestCommon.SetupTestSource(false); + WinGetSettingsHelper.ConfigureFeature("configureExport", true); + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPackageExport -v 1.0.0.0 --silent -l {installDir}"); + } + + /// <summary> + /// Tear down. + /// </summary> + [OneTimeTearDown] + public void BaseTeardown() + { + TestCommon.TearDownTestSource(); + WinGetSettingsHelper.ConfigureFeature("configureExport", false); + TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestPackageExport"); + } + + /// <summary> + /// Export a specific package. + /// </summary> + [Test] + public void ExportTestPackage() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--package-id AppInstallerTest.TestPackageExport -o {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(File.Exists(exportFile)); + + // Check exported file is readable and validate content + var showResult = TestCommon.RunAICLICommand(ShowCommand, $"-f {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, showResult.ExitCode); + Assert.True(showResult.StdOut.Contains("WinGetSource")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_{Constants.TestSourceType}]")); + Assert.True(showResult.StdOut.Contains($"type: {Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains($"argument: {Constants.TestSourceUrl}")); + Assert.True(showResult.StdOut.Contains($"name: {Constants.TestSourceName}")); + + Assert.True(showResult.StdOut.Contains("WinGetPackage")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_AppInstallerTest.TestPackageExport]")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_{Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains("id: AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains($"source: {Constants.TestSourceName}")); + } + + /// <summary> + /// Export a specific package with version. + /// </summary> + [Test] + public void ExportTestPackageWithVersion() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--package-id AppInstallerTest.TestPackageExport --include-versions -o {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(File.Exists(exportFile)); + + // Check exported file is readable and validate content + var showResult = TestCommon.RunAICLICommand(ShowCommand, $"-f {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, showResult.ExitCode); + Assert.True(showResult.StdOut.Contains("WinGetSource")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_{Constants.TestSourceType}]")); + Assert.True(showResult.StdOut.Contains($"type: {Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains($"argument: {Constants.TestSourceUrl}")); + Assert.True(showResult.StdOut.Contains($"name: {Constants.TestSourceName}")); + + Assert.True(showResult.StdOut.Contains("WinGetPackage")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_AppInstallerTest.TestPackageExport]")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_{Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains("id: AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains($"source: {Constants.TestSourceName}")); + Assert.True(showResult.StdOut.Contains("version: 1.0.0.0")); + } + + /// <summary> + /// Export all. + /// </summary> + [Test] + public void ExportAll() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--all -o {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(File.Exists(exportFile)); + + // Check exported file is readable and validate content + var showResult = TestCommon.RunAICLICommand(ShowCommand, $"-f {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, showResult.ExitCode); + Assert.True(showResult.StdOut.Contains("WinGetSource")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_{Constants.TestSourceType}]")); + Assert.True(showResult.StdOut.Contains($"type: {Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains($"argument: {Constants.TestSourceUrl}")); + Assert.True(showResult.StdOut.Contains($"name: {Constants.TestSourceName}")); + + Assert.True(showResult.StdOut.Contains("WinGetPackage")); + Assert.True(showResult.StdOut.Contains($"[{Constants.TestSourceName}_AppInstallerTest.TestPackageExport]")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_{Constants.TestSourceType}")); + Assert.True(showResult.StdOut.Contains("id: AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains($"source: {Constants.TestSourceName}")); + } + + /// <summary> + /// Export a specific package that's not installed. + /// </summary> + [Test] + public void ExportFailedWithNotFoundPackage() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--package-id NotFound.NotFound -o {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + } + + /// <summary> + /// Export all with specific package id. + /// </summary> + [Test] + public void ExportFailedWithAllAndSpecificPackage() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--all --package-id AppInstallerTest.TestPackageExport -o {exportFile}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_INVALID_CL_ARGUMENTS, result.ExitCode); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml @@ -0,0 +1,20 @@ +Id: AppInstallerTest.TestPackageExport +Name: TestPackageExport +Version: 1.0.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + ProductCode: '{92e3d4e5-6e3d-4ae4-b9f0-b7e0a5f25b91}' + Switches: + Custom: '/ProductID {92e3d4e5-6e3d-4ae4-b9f0-b7e0a5f25b91} /DisplayName TestPackageExport' + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -861,7 +861,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Ignore unavailable packages</value> </data> <data name="ExportIncludeVersionsArgumentDescription" xml:space="preserve"> - <value>Include package versions in produced file</value> + <value>Include package versions in export file</value> </data> <data name="ImportIgnoreVersionsArgumentDescription" xml:space="preserve"> <value>Ignore package versions from import file</value> @@ -2976,12 +2976,16 @@ Please specify one of them using the --source option to proceed.</value> <data name="ConfigurationGettingResourceSettings" xml:space="preserve"> <value>Getting configuration settings...</value> </data> - <data name="ConfigureExportArgumentError" xml:space="preserve"> - <value>At least --packageId and/or --module with --resource must be provided</value> - <comment>{Locked="--packageId,--module, --resource"}</comment> + <data name="ConfigureExportArgumentRequiredError" xml:space="preserve"> + <value>At least --packageId and/or --module with --resource must be provided. Or use --all to export all package configurations.</value> + <comment>{Locked="--packageId,--module, --resource, --all"}</comment> + </data> + <data name="ConfigureExportArgumentConflictWithAllError" xml:space="preserve"> + <value>Arguments --packageId, --module and --resource cannot be used with --all.</value> + <comment>{Locked="--packageId,--module, --resource, --all"}</comment> </data> <data name="ConfigureExportCommandLongDescription" xml:space="preserve"> - <value>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.</value> + <value>Exports configuration resources to a configuration file. When used with --all, exports all package configurations. 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.</value> <comment>{Locked="WinGetPackage,--packageId,--module, --resource"}</comment> </data> <data name="ConfigureExportCommandShortDescription" xml:space="preserve"> @@ -2996,6 +3000,9 @@ Please specify one of them using the --source option to proceed.</value> <data name="ConfigureExportResource" xml:space="preserve"> <value>The configuration resource to export.</value> </data> + <data name="ConfigureExportAll" xml:space="preserve"> + <value>Exports all package configurations.</value> + </data> <data name="WINGET_CONFIG_ERROR_GET_FAILED" xml:space="preserve"> <value>The configuration unit failed getting its properties.</value> </data> diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -155,6 +155,9 @@ namespace AppInstaller::Repository bool Explicit = false; }; + // Check if a source matches a well known source + std::optional<WellKnownSource> CheckForWellKnownSource(const SourceDetails& sourceDetails); + // Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry. struct SourceAgreement { diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -427,6 +427,11 @@ namespace AppInstaller::Repository } } + std::optional<WellKnownSource> CheckForWellKnownSource(const SourceDetails& sourceDetails) + { + return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type); + } + Source::Source() {} Source::Source(std::string_view name)