winget-cli

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

commit 3f1ede7378fbfefb7a4574965e32923f7a6ad35d
parent 48de219db689090d3ead9573f29b77b6352da00e
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Fri,  2 May 2025 23:44:23 -0700

Add support for exporting package related DSC v3 resources and some predefined resources in configure export all  (#5428)

- Break existing CreateConfigurationUnit function to 3 steps: Create
unit, Export unit and Add dependent unit to unit
- Added support for exporting individual package settings if a dsc v3
resources could be found for that package
- Added support for exporting some predefined resources
Diffstat:
M.github/actions/spelling/expect.txt | 1+
Mazure-pipelines.yml | 7+++++++
Msrc/AppInstallerCLICore/Commands/DebugCommand.cpp | 1+
Msrc/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp | 26+++++++++++++-------------
Msrc/AppInstallerCLICore/ConfigureExportCommand.cpp | 3+--
Msrc/AppInstallerCLICore/PackageCollection.h | 6+++---
Msrc/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h | 2++
Msrc/AppInstallerCLICore/Resources.h | 8++++++++
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 514+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.h | 4+++-
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 14++++++--------
Msrc/AppInstallerCLIE2ETests/ConfigureExportCommand.cs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml | 2+-
Msrc/AppInstallerCLIPackage/Package.appxmanifest | 1+
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 26++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp | 12++++++++++++
Msrc/AppInstallerSharedLib/AppInstallerStrings.cpp | 22++++++++++++++++++++++
Msrc/AppInstallerSharedLib/Filesystem.cpp | 5+++++
Msrc/AppInstallerSharedLib/Public/AppInstallerStrings.h | 9++++++++-
Msrc/AppInstallerSharedLib/Public/winget/Filesystem.h | 3+++
Msrc/AppInstallerSharedLib/pch.h | 3++-
Msrc/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs | 2+-
Msrc/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs | 35++++++++++++++++++++++++++---------
Msrc/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs | 7++++++-
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.cpp | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.h | 5+++++
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl | 12++++++++++++
27 files changed, 735 insertions(+), 128 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -595,6 +595,7 @@ wingetutil winreg winrtact winstring +WMI Wnd WNDCLASS WNDCLASSEX diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -379,6 +379,13 @@ jobs: displayName: Install DSC v3 condition: succeededOrFailed() + # Install required DSC modules until export all command can handle auto acquisition + - pwsh: | + Install-Module -Name Microsoft.WinGet.DSC -Force + Install-Module -Name Microsoft.Windows.Developer -AllowPrerelease -Force + displayName: Install Required DSC Modules for Tests + condition: succeededOrFailed() + - task: PowerShell@2 displayName: Run Unit Tests Packaged inputs: diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -106,6 +106,7 @@ namespace AppInstaller::CLI OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationEnvironment>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::IConfigurationUnitProcessorDetails>>(context); OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IConfigurationUnitProcessorDetails2>(context); + OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IConfigurationUnitProcessorDetails3>(context); OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IGetAllSettingsConfigurationUnitProcessor>(context); OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IGetAllUnitsConfigurationUnitProcessor>(context); OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IFindUnitProcessorsSetProcessor>(context); diff --git a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp @@ -28,19 +28,6 @@ namespace AppInstaller::CLI::ConfigurationRemoting // The string used to divide the arguments sent to the remote server constexpr std::wstring_view s_ArgumentsDivider = L"\n~~~~~~\n"sv; - std::wstring_view ToString(ProcessorEngine value) - { - switch (value) - { - case ProcessorEngine::PowerShell: - return s_ProcessorEngine_PowerShell; - case ProcessorEngine::DSCv3: - return s_ProcessorEngine_DSCv3; - default: - THROW_HR(E_UNEXPECTED); - } - } - // A helper with a convenient function that we use to receive the remote factory object. struct RemoteFactoryCallback : winrt::implements<RemoteFactoryCallback, IConfigurationStatics> { @@ -373,6 +360,19 @@ namespace AppInstaller::CLI::ConfigurationRemoting } } + std::wstring_view ToString(ProcessorEngine value) + { + switch (value) + { + case ProcessorEngine::PowerShell: + return s_ProcessorEngine_PowerShell; + case ProcessorEngine::DSCv3: + return s_ProcessorEngine_DSCv3; + default: + THROW_HR(E_UNEXPECTED); + } + } + winrt::hstring ToHString(PropertyName name) { switch (name) diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -44,9 +44,8 @@ namespace AppInstaller::CLI { context << VerifyIsFullPackage << - SearchSourceForPackageExport << CreateConfigurationProcessorWithoutFactory << - CreateOrOpenConfigurationSet{} << + CreateOrOpenConfigurationSet{ "0.3", context.Args.Contains(Execution::Args::Type::ConfigurationExportAll) } << CreateConfigurationProcessor << PopulateConfigurationSetForExport << WriteConfigFile; diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h @@ -28,7 +28,8 @@ namespace AppInstaller::CLI Utility::LocIndString Id; Utility::VersionAndChannel VersionAndChannel; - Manifest::ScopeEnum Scope = Manifest::ScopeEnum::Unknown; + Manifest::ScopeEnum Scope = Manifest::ScopeEnum::Unknown; + std::filesystem::path InstalledLocation; }; // A source along with a set of packages available from it. @@ -76,4 +77,4 @@ namespace AppInstaller::CLI // Tries to parse a JSON into a collection of packages. ParseResult TryParseJson(const Json::Value& root); } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h b/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h @@ -17,6 +17,8 @@ namespace AppInstaller::CLI::ConfigurationRemoting DSCv3, }; + std::wstring_view ToString(ProcessorEngine value); + // Determines the appropriate processor engine to use for the given configuration set. ProcessorEngine DetermineProcessorEngine(winrt::Microsoft::Management::Configuration::ConfigurationSet set); diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -57,6 +57,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAcceptWarningArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAllUsersElevated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationApply); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationApplyingUnit); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAssert); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDescriptionWasTruncated); @@ -69,7 +70,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportAddingToFile); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailedToGetUnitProcessors); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportingUnit); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportInstallRequiredModule); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportInstallRequiredModuleFailed); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportSuccessful); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportUnitStart); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportUnitFailed); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToGetDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); @@ -83,6 +90,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileVersionUnknown); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingResourceSettings); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingUnitProcessors); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryEmpty); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryItemArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryItemNotFound); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -38,6 +38,8 @@ namespace AppInstaller::CLI::Workflow namespace anon { + static const AppInstaller::Utility::Version s_MinimumSchemaVersionModuleNameRequiredInType = { "0.3" }; + 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"; @@ -45,6 +47,10 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_Unit_WinGetPackage = L"WinGetPackage"; constexpr std::wstring_view s_Unit_WinGetSource = L"WinGetSource"; + constexpr std::wstring_view s_UnitType_WinGetPackage_DSCv3 = L"Microsoft.WinGet/Package"; + constexpr std::wstring_view s_UnitType_WinGetSource_DSCv3 = L"Microsoft.WinGet/Source"; + constexpr std::wstring_view s_UnitType_PowerShellModuleGet = L"PowerShellGet/PSModule"; + constexpr std::wstring_view s_Module_WinGetClient = L"Microsoft.WinGet.DSC"; constexpr std::wstring_view s_Setting_WinGetPackage_Id = L"id"; @@ -55,6 +61,26 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_Setting_WinGetSource_Arg = L"argument"; constexpr std::wstring_view s_Setting_WinGetSource_Type = L"type"; + constexpr std::wstring_view s_Setting_PowerShellGet_ModuleName = L"name"; + + struct PredefinedResource + { + // RequiredModule could be empty, meaning no required modules needed. + std::wstring RequiredModule; + + std::vector<std::wstring> UnitTypes; + }; + + static const PredefinedResource s_PredefinedResourcesForExport[] = { + { std::wstring{ s_Module_WinGetClient }, { L"Microsoft.WinGet.DSC/WinGetUserSettings" } }, + { L"Microsoft.Windows.Developer", { L"Microsoft.Windows.Developer/DeveloperMode", L"Microsoft.Windows.Developer/EnableDarkMode", L"Microsoft.Windows.Developer/ShowSecondsInClock", L"Microsoft.Windows.Developer/Taskbar", L"Microsoft.Windows.Developer/WindowsExplorer" }}, + }; + + static const std::wstring s_PackageSettingsExclusionList[] = { + L"Microsoft.WinGet/", L"Microsoft.DSC.Debug/", L"Microsoft.DSC/", L"Microsoft.DSC.Transitional/", L"Microsoft.Windows/RebootPending", + L"Microsoft.Windows/Registry", L"Microsoft.Windows/WMI", L"Microsoft.Windows/WindowsPowerShell", L"Microsoft/OSInfo" + }; + Logging::Level ConvertLevel(DiagnosticLevel level) { switch (level) @@ -1116,20 +1142,103 @@ namespace AppInstaller::CLI::Workflow context.Get<Data::ConfigurationContext>().Set(result); } - ConfigurationUnit CreateWinGetSourceUnit(const PackageCollection::Source& source) + ConfigurationUnit CreateConfigurationUnitFromModuleResource(std::string_view moduleName, std::string_view resourceName, std::string_view descriptionResourceName, const Utility::Version& schemaVersion) + { + std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); + std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); + + ConfigurationUnit unit; + unit.Type(schemaVersion >= s_MinimumSchemaVersionModuleNameRequiredInType ? moduleNameWide + L'/' + resourceNameWide : resourceNameWide); + unit.Identifier(unit.Type() + L'_' + Utility::ConvertToUTF16(Utility::GetRandomString())); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + + Utility::LocIndString description; + if (!descriptionResourceName.empty()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ descriptionResourceName }); + } + else + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); + } + + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + return unit; + } + + ConfigurationUnit CreateConfigurationUnitFromUnitType(std::wstring_view unitType, std::string_view descriptionResourceName = "") + { + ConfigurationUnit unit; + unit.Type(unitType); + unit.Identifier(unit.Type() + L'_' + Utility::ConvertToUTF16(Utility::GetRandomString())); + + ValueSet directives; + Utility::LocIndString description; + if (!descriptionResourceName.empty()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ descriptionResourceName }); + } + else + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(unitType) }); + } + + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + return unit; + } + + ConfigurationUnit CreatePowerShellModuleGetUnit(const std::wstring& moduleName) + { + ConfigurationUnit unit = CreateConfigurationUnitFromUnitType(s_UnitType_PowerShellModuleGet, Utility::ConvertToUTF8(moduleName)); + + ValueSet settings; + settings.Insert(s_Setting_PowerShellGet_ModuleName, PropertyValue::CreateString(moduleName)); + unit.Settings(settings); + + return unit; + } + + std::wstring GetWinGetSourceUnitType(const ConfigurationContext& configContext) + { + Utility::Version schemaVersion = { Utility::ConvertToUTF8(configContext.Set().SchemaVersion()) }; + ConfigurationRemoting::ProcessorEngine processorEngine = ConfigurationRemoting::DetermineProcessorEngine(configContext.Set()); + + if (schemaVersion >= s_MinimumSchemaVersionModuleNameRequiredInType) + { + if (processorEngine == ConfigurationRemoting::ProcessorEngine::DSCv3) + { + return std::wstring{ s_UnitType_WinGetSource_DSCv3 }; + } + else + { + return std::wstring{ s_Module_WinGetClient } + L'/' + std::wstring{ s_Unit_WinGetSource }; + } + } + else + { + return std::wstring{ s_Unit_WinGetSource }; + } + } + + ConfigurationUnit CreateWinGetSourceUnit(const PackageCollection::Source& source, std::wstring_view unitType) { std::string sourceUnitId = source.Details.Name + '_' + source.Details.Type; std::wstring sourceUnitIdWide = Utility::ConvertToUTF16(sourceUnitId); ConfigurationUnit unit; - unit.Type(s_Unit_WinGetSource); + unit.Type(unitType); 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); @@ -1144,20 +1253,41 @@ namespace AppInstaller::CLI::Workflow return unit; } - ConfigurationUnit CreateWinGetPackageUnit(const PackageCollection::Package& package, const PackageCollection::Source& source, bool includeVersion, const std::optional<ConfigurationUnit>& dependentUnit) + std::wstring GetWinGetPackageUnitType(const ConfigurationContext& configContext) + { + Utility::Version schemaVersion = { Utility::ConvertToUTF8(configContext.Set().SchemaVersion()) }; + ConfigurationRemoting::ProcessorEngine processorEngine = ConfigurationRemoting::DetermineProcessorEngine(configContext.Set()); + + if (schemaVersion >= s_MinimumSchemaVersionModuleNameRequiredInType) + { + if (processorEngine == ConfigurationRemoting::ProcessorEngine::DSCv3) + { + return std::wstring{ s_UnitType_WinGetPackage_DSCv3 }; + } + else + { + return std::wstring{ s_Module_WinGetClient } + L'/' + std::wstring{ s_Unit_WinGetPackage }; + } + } + else + { + return std::wstring{ s_Unit_WinGetPackage }; + } + } + + ConfigurationUnit CreateWinGetPackageUnit(const PackageCollection::Package& package, const PackageCollection::Source& source, bool includeVersion, const std::optional<ConfigurationUnit>& dependentUnit, std::wstring_view unitType) { std::wstring packageIdWide = Utility::ConvertToUTF16(package.Id); std::wstring sourceNameWide = Utility::ConvertToUTF16(source.Details.Name); ConfigurationUnit unit; - unit.Type(s_Unit_WinGetPackage); + unit.Type(unitType); unit.Identifier(sourceNameWide + L'_' + packageIdWide); unit.Intent(ConfigurationUnitIntent::Apply); auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ package.Id }); 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); @@ -1183,6 +1313,25 @@ namespace AppInstaller::CLI::Workflow return unit; } + ApplyConfigurationUnitResult ApplyUnit(Execution::Context& context, ConfigurationUnit& unit) + { + unit.Intent(ConfigurationUnitIntent::Apply); + + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationApplyingUnit()); + + ApplyConfigurationUnitResult applyResult = nullptr; + { + auto applyAction = context.Get<Data::ConfigurationContext>().Processor().ApplyUnitAsync(unit); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { applyAction.Cancel(); }); + applyResult = applyAction.get(); + } + + progressScope.reset(); + return applyResult; + } + GetConfigurationUnitSettingsResult GetUnitSettings(Execution::Context& context, ConfigurationUnit& unit) { // This assumes there are no required properties for Get, but for example WinGetPackage requires the Id. @@ -1205,76 +1354,300 @@ namespace AppInstaller::CLI::Workflow return getResult; } - ConfigurationUnit CreateConfigurationUnit(Execution::Context& context, std::string_view moduleName, std::string_view resourceName, const std::optional<ConfigurationUnit>& dependentUnit) + GetAllConfigurationUnitsResult GetAllUnits(Execution::Context& context, ConfigurationUnit& unit) { - std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); - std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); + unit.Intent(ConfigurationUnitIntent::Inform); - ConfigurationUnit unit; - unit.Type(resourceNameWide); + auto progressScope = context.Reporter.BeginAsyncProgress(true); - ValueSet directives; - directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationExportingUnit()); - Utility::LocIndString description; - if (dependentUnit.has_value()) + GetAllConfigurationUnitsResult getResult = nullptr; + { + auto getAction = context.Get<Data::ConfigurationContext>().Processor().GetAllUnitsAsync(unit); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { getAction.Cancel(); }); + getResult = getAction.get(); + } + + progressScope.reset(); + return getResult; + } + + std::vector<ConfigurationUnit> ExportUnit(Execution::Context& context, ConfigurationUnit& unit, bool throwOnFailure = false) + { + std::vector<ConfigurationUnit> result; + + context.Reporter.Info() << Resource::String::ConfigurationExportUnitStart(Utility::LocIndView{ Utility::ConvertToUTF8(unit.Type()) }) << std::endl; + + // Try export first + auto exportResult = GetAllUnits(context, unit); + auto exportResultCode = exportResult.ResultInformation().ResultCode(); + if (SUCCEEDED(exportResultCode)) { - description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependentUnit.value().Identifier()) }); + for (auto resultUnit : exportResult.Units()) + { + result.emplace_back(std::move(resultUnit)); + } } else { - description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); + AICLI_LOG(Config, Warning, << "Failed GetAllUnits. Will try GetUnitSettings."); + LogFailedGetConfigurationUnitDetails(unit, exportResult.ResultInformation()); + + // Try GetUnitSettings if export failed. + auto getResult = GetUnitSettings(context, unit); + auto getResultCode = getResult.ResultInformation().ResultCode(); + if (getResultCode == WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY) + { + // Retry if it fails with not found in the case the module is a pre-released one. + AICLI_LOG(Config, Info, << "Failed GetUnitSettings because module not found. Will try allow prerelease."); + auto directives = unit.Metadata(); + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + getResult = GetUnitSettings(context, unit); + } + + if (FAILED(getResult.ResultInformation().ResultCode())) + { + AICLI_LOG(Config, Error, << "Failed Get Unit Settings"); + LogFailedGetConfigurationUnitDetails(unit, getResult.ResultInformation()); + + if (throwOnFailure) + { + context.Reporter.Error() << Resource::String::ConfigurationExportUnitFailed << std::endl; + OutputUnitRunFailure(context, unit, getResult.ResultInformation()); + THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + } + else + { + context.Reporter.Warn() << Resource::String::ConfigurationExportUnitFailed << std::endl; + } + } + else + { + unit.Settings(getResult.Settings()); + result.emplace_back(unit); + } } - directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); - unit.Metadata(directives); + return result; + } + + void AddDependentUnit(std::vector<ConfigurationUnit>& units, const ConfigurationUnit& dependentUnit) + { + for (auto& unit : units) + { + unit.Dependencies().Append(dependentUnit.Identifier()); + } + } - // Call processor to get settings for the unit. - auto getResult = GetUnitSettings(context, unit); - winrt::hresult resultCode = getResult.ResultInformation().ResultCode(); - if (FAILED(resultCode)) + std::vector<IConfigurationUnitProcessorDetails> GetAllUnitProcessors(Execution::Context& context) + { + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + std::vector<IConfigurationUnitProcessorDetails> result; + + // Only supported by dsc v3 processor. + if (ConfigurationRemoting::ProcessorEngine::DSCv3 == ConfigurationRemoting::DetermineProcessorEngine(configContext.Set())) { - // 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) + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationGettingUnitProcessors()); + { - directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); - unit.Metadata(directives); + FindUnitProcessorsOptions findOptions; + findOptions.UnitDetailFlags(ConfigurationUnitDetailFlags::Local); + auto findAction = context.Get<Data::ConfigurationContext>().Processor().FindUnitProcessorsAsync(findOptions); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { findAction.Cancel(); }); + for (auto unitProcessor : findAction.get()) + { + result.emplace_back(std::move(unitProcessor)); + } + } + + progressScope.reset(); + } - auto preReleaseResult = GetUnitSettings(context, unit); - if (SUCCEEDED(preReleaseResult.ResultInformation().ResultCode())) + return result; + } + + void ExportPredefinedResources(Execution::Context& context) + { + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + + for (const auto& resources : s_PredefinedResourcesForExport) + { + std::optional<ConfigurationUnit> requiredModuleUnit; + + /* The PowershellGet/PSModule does not work under dsc v3 adaptor yet. + * Uncomment if still applicable after the issue is fixed. + if (!resources.RequiredModule.empty()) + { + requiredModuleUnit = CreatePowerShellModuleGetUnit(resources.RequiredModule); + + // Apply the unit to make sure it's on the system. + context.Reporter.Info() << Resource::String::ConfigurationExportInstallRequiredModule(Utility::LocIndView{ Utility::ConvertToUTF8(resources.RequiredModule) }) << std::endl; + auto applyResult = ApplyUnit(context, requiredModuleUnit.value()); + if (SUCCEEDED(applyResult.ResultInformation().ResultCode())) { - isPreRelease = true; - getResult = preReleaseResult; + configContext.Set().Units().Append(requiredModuleUnit.value()); } else { - AICLI_LOG(Config, Error, << "Failed Get allowing prerelease modules"); - LogFailedGetConfigurationUnitDetails(unit, preReleaseResult.ResultInformation()); + AICLI_LOG(Config, Warning, << "Failed to ensure module. [" << Utility::ConvertToUTF8(resources.RequiredModule) << "] Related settings will not be exported."); + LogFailedGetConfigurationUnitDetails(requiredModuleUnit.value(), applyResult.ResultInformation()); + context.Reporter.Warn() << Resource::String::ConfigurationExportInstallRequiredModuleFailed << std::endl; + continue; } } + */ - if (!isPreRelease) + for (const auto& resourceType : resources.UnitTypes) { - OutputUnitRunFailure(context, unit, getResult.ResultInformation()); - THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + auto resourceUnit = CreateConfigurationUnitFromUnitType(resourceType); + auto exportedUnits = ExportUnit(context, resourceUnit); + + if (requiredModuleUnit) + { + AddDependentUnit(exportedUnits, requiredModuleUnit.value()); + } + + for (auto exportedUnit : exportedUnits) + { + configContext.Set().Units().Append(std::move(exportedUnit)); + } } } + } - unit.Settings(getResult.Settings()); + void ProcessPackagesForConfigurationExportAll(Execution::Context& context) + { + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + std::wstring sourceUnitType = GetWinGetSourceUnitType(configContext); + std::wstring packageUnitType = GetWinGetPackageUnitType(configContext); - // GetUnitSettings will set it to Inform. - unit.Intent(ConfigurationUnitIntent::Apply); + // This will be later used by per package settings export. + std::vector<IConfigurationUnitProcessorDetails> unitProcessors; + try + { + unitProcessors = GetAllUnitProcessors(context); + } + catch (...) + { + AICLI_LOG(Config, Warning, << "Finding unit processors failed. Individual package settings will not be exported."); + context.Reporter.Warn() << Resource::String::ConfigurationExportFailedToGetUnitProcessors << std::endl; + } - // Add dependency if needed. - if (dependentUnit.has_value()) + // Filter out processors in exclusion list. + for (auto itr = unitProcessors.begin(); itr != unitProcessors.end(); /* itr incremented in the logic */) { - auto dependencies = winrt::single_threaded_vector<winrt::hstring>(); - dependencies.Append(dependentUnit.value().Identifier()); - unit.Dependencies(std::move(dependencies)); + bool processorRemoved = false; + for (const auto& exclusionItem : anon::s_PackageSettingsExclusionList) + { + if (Utility::CaseInsensitiveStartsWith(itr->UnitType(), exclusionItem)) + { + itr = unitProcessors.erase(itr); + processorRemoved = true; + break; + } + } + + if (!processorRemoved) + { + itr++; + } } - return unit; + for (const auto& source : context.Get<Execution::Data::PackageCollection>().Sources) + { + // Create WinGetSource unit for non well known source. + std::optional<ConfigurationUnit> sourceUnit; + if (!CheckForWellKnownSource(source.Details)) + { + sourceUnit = anon::CreateWinGetSourceUnit(source, sourceUnitType); + 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, packageUnitType); + configContext.Set().Units().Append(packageUnit); + + // Try package settings export. + for (auto itr = unitProcessors.begin(); itr != unitProcessors.end(); /* itr incremented in the logic */) + { + IConfigurationUnitProcessorDetails3 unitProcessor3; + itr->try_as(unitProcessor3); + if (Filesystem::IsParentPath(std::filesystem::path{ std::wstring{ unitProcessor3.Path() } }, package.InstalledLocation)) + { + ConfigurationUnit configUnit = anon::CreateConfigurationUnitFromUnitType( + unitProcessor3.UnitType(), + Utility::ConvertToUTF8(packageUnit.Identifier())); + + auto exportedUnits = anon::ExportUnit(context, configUnit); + anon::AddDependentUnit(exportedUnits, packageUnit); + + for (auto exportedUnit : exportedUnits) + { + configContext.Set().Units().Append(exportedUnit); + } + + // Remove the unit processor from the list after export. + itr = unitProcessors.erase(itr); + } + else + { + itr++; + } + } + } + } + } + + void ProcessPackagesForConfigurationExportSingle(Execution::Context& context) + { + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + + // When exporting single WinGetPackage unit, the WinGetPackage unit can be used as a dependent unit for following configuration unit. + std::optional<ConfigurationUnit> singlePackageUnit; + + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + const auto& exportSources = context.Get<Execution::Data::PackageCollection>().Sources; + // There should be 1 package under 1 source. + THROW_HR_IF(E_UNEXPECTED, exportSources.size() != 1 || exportSources[0].Packages.size() != 1); + + std::optional<ConfigurationUnit> sourceUnit; + if (!CheckForWellKnownSource(exportSources[0].Details)) + { + sourceUnit = anon::CreateWinGetSourceUnit(exportSources[0], GetWinGetSourceUnitType(configContext)); + configContext.Set().Units().Append(sourceUnit.value()); + } + + singlePackageUnit = anon::CreateWinGetPackageUnit(exportSources[0].Packages[0], exportSources[0], context.Args.Contains(Args::Type::IncludeVersions), sourceUnit, GetWinGetPackageUnitType(configContext)); + configContext.Set().Units().Append(singlePackageUnit.value()); + } + + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) + { + auto configUnit = anon::CreateConfigurationUnitFromModuleResource( + context.Args.GetArg(Args::Type::ConfigurationExportModule), + context.Args.GetArg(Args::Type::ConfigurationExportResource), + singlePackageUnit ? Utility::ConvertToUTF8(singlePackageUnit->Identifier()) : "", + Utility::Version{ Utility::ConvertToUTF8(configContext.Set().SchemaVersion()) }); + + auto exportedUnits = anon::ExportUnit(context, configUnit, true); + + if (singlePackageUnit) + { + anon::AddDependentUnit(exportedUnits, singlePackageUnit.value()); + } + + for (auto exportedUnit : exportedUnits) + { + configContext.Set().Units().Append(exportedUnit); + } + } } bool HistorySetMatchesInput(const ConfigurationSet& set, const std::string& foldedInput) @@ -1411,7 +1784,7 @@ namespace AppInstaller::CLI::Workflow { std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; - if (std::filesystem::exists(argPath)) + if (std::filesystem::exists(argPath) && !m_createAlways) { anon::OpenConfigurationSet(context, argPath, false); } @@ -1420,6 +1793,11 @@ namespace AppInstaller::CLI::Workflow ConfigurationSet set; set.SchemaVersion(Utility::ConvertToUTF16(m_defaultSchemaVersion)); + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ConfigurationDSCv3)) + { + set.Environment().ProcessorIdentifier(ConfigurationRemoting::ToString(ConfigurationRemoting::ProcessorEngine::DSCv3)); + } + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); anon::SetNameAndOrigin(set, absolutePath); @@ -1938,42 +2316,20 @@ namespace AppInstaller::CLI::Workflow void PopulateConfigurationSetForExport(Execution::Context& context) { - ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); - - // 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; + bool isExportAll = context.Args.Contains(Execution::Args::Type::ConfigurationExportAll); - for (const auto& source : context.Get<Execution::Data::PackageCollection>().Sources) + if (isExportAll) { - // 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; - } - } + context << + anon::ExportPredefinedResources << + SearchSourceForPackageExport << + anon::ProcessPackagesForConfigurationExportAll; } - - if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) + else { - auto configUnit = anon::CreateConfigurationUnit( - context, - context.Args.GetArg(Args::Type::ConfigurationExportModule), - context.Args.GetArg(Args::Type::ConfigurationExportResource), - singlePackageUnit); - - configContext.Set().Units().Append(configUnit); + context << + SearchSourceForPackageExport << + anon::ProcessPackagesForConfigurationExportSingle; } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -29,12 +29,14 @@ namespace AppInstaller::CLI::Workflow // Outputs: ConfigurationSet struct CreateOrOpenConfigurationSet : public WorkflowTask { - CreateOrOpenConfigurationSet(std::string defaultSchemaVersion = "0.2") : WorkflowTask("CreateOrOpenConfigurationSet"), m_defaultSchemaVersion(std::move(defaultSchemaVersion)) {} + CreateOrOpenConfigurationSet(std::string defaultSchemaVersion, bool createAlways = false) : + WorkflowTask("CreateOrOpenConfigurationSet"), m_defaultSchemaVersion(std::move(defaultSchemaVersion)), m_createAlways(createAlways) {} void operator()(Execution::Context& context) const override; private: std::string m_defaultSchemaVersion; + bool m_createAlways = false; }; // Outputs the configuration set. diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -138,17 +138,15 @@ namespace AppInstaller::CLI::Workflow // Take the Id from the available package because that is the one used in the source, // but take the exported version from the installed package if needed. + PackageCollection::Package exportPackage; + exportPackage.Id = availablePackageVersion->GetProperty(PackageVersionProperty::Id); + exportPackage.InstalledLocation = Utility::ConvertToUTF16(installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledLocation]); if (includeVersions) { - sourceItr->Packages.emplace_back( - availablePackageVersion->GetProperty(PackageVersionProperty::Id), - version.get(), - channel.get()); - } - else - { - sourceItr->Packages.emplace_back(availablePackageVersion->GetProperty(PackageVersionProperty::Id)); + exportPackage.VersionAndChannel = { version.get(), channel.get() }; } + + sourceItr->Packages.emplace_back(std::move(exportPackage)); } context.Add<Execution::Data::PackageCollection>(std::move(exportedPackages)); diff --git a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs @@ -19,6 +19,8 @@ namespace AppInstallerCLIE2ETests private const string Command = "configure export"; private const string ShowCommand = "configure show"; + private string previousPathValue = string.Empty; + /// <summary> /// Set up. /// </summary> @@ -27,8 +29,11 @@ namespace AppInstallerCLIE2ETests { TestCommon.SetupTestSource(false); WinGetSettingsHelper.ConfigureFeature("configureExport", true); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); var installDir = TestCommon.GetRandomTestDir(); TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPackageExport -v 1.0.0.0 --silent -l {installDir}"); + this.previousPathValue = System.Environment.GetEnvironmentVariable("PATH"); + System.Environment.SetEnvironmentVariable("PATH", this.previousPathValue + ";" + installDir); } /// <summary> @@ -40,6 +45,11 @@ namespace AppInstallerCLIE2ETests TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestPackageExport"); TestCommon.TearDownTestSource(); WinGetSettingsHelper.ConfigureFeature("configureExport", false); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + if (!string.IsNullOrEmpty(this.previousPathValue)) + { + System.Environment.SetEnvironmentVariable("PATH", this.previousPathValue); + } } /// <summary> @@ -57,13 +67,13 @@ namespace AppInstallerCLIE2ETests // 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("Microsoft.WinGet/Source")); 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("Microsoft.WinGet/Package")); 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")); @@ -71,6 +81,38 @@ namespace AppInstallerCLIE2ETests } /// <summary> + /// Export a specific package with related configuration. + /// </summary> + [Test] + public void ExportTestPackageWithPackageSettings() + { + var exportDir = TestCommon.GetRandomTestDir(); + var exportFile = Path.Combine(exportDir, "exported.yml"); + var result = TestCommon.RunAICLICommand(Command, $"--package-id AppInstallerTest.TestPackageExport --module AppInstallerTest --resource TestResource -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("Microsoft.WinGet/Source")); + 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("Microsoft.WinGet/Package")); + 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("AppInstallerTest/TestResource")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains("data: TestData")); + } + + /// <summary> /// Export a specific package with version. /// </summary> [Test] @@ -85,13 +127,13 @@ namespace AppInstallerCLIE2ETests // 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("Microsoft.WinGet/Source")); 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("Microsoft.WinGet/Package")); 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")); @@ -107,24 +149,33 @@ namespace AppInstallerCLIE2ETests { var exportDir = TestCommon.GetRandomTestDir(); var exportFile = Path.Combine(exportDir, "exported.yml"); - var result = TestCommon.RunAICLICommand(Command, $"--all -o {exportFile}"); + var result = TestCommon.RunAICLICommand(Command, $"--all -o {exportFile}", timeOut: 1200000); 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}"); + var showResult = TestCommon.RunAICLICommand(ShowCommand, $"-f {exportFile}", timeOut: 1200000); Assert.AreEqual(Constants.ErrorCode.S_OK, showResult.ExitCode); - Assert.True(showResult.StdOut.Contains("WinGetSource")); + + Assert.True(showResult.StdOut.Contains("Microsoft.WinGet.DSC/WinGetUserSettings")); + Assert.True(showResult.StdOut.Contains("Microsoft.Windows.Developer/DeveloperMode")); + Assert.True(showResult.StdOut.Contains("Microsoft.Windows.Developer/EnableDarkMode")); + + Assert.True(showResult.StdOut.Contains("Microsoft.WinGet/Source")); 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("Microsoft.WinGet/Package")); 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("AppInstallerTest/TestResource")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains("data: TestData")); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstallerForExport.yaml @@ -10,7 +10,7 @@ Installers: InstallerType: exe ProductCode: '{92e3d4e5-6e3d-4ae4-b9f0-b7e0a5f25b91}' Switches: - Custom: '/ProductID {92e3d4e5-6e3d-4ae4-b9f0-b7e0a5f25b91} /DisplayName TestPackageExport' + Custom: '/ProductID {92e3d4e5-6e3d-4ae4-b9f0-b7e0a5f25b91} /DisplayName TestPackageExport /GenerateDscResourceFiles' SilentWithProgress: /exeswp Silent: /exesilent Interactive: /exeinteractive diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -116,6 +116,7 @@ <Interface Name="Windows.Foundation.Collections.IIterable`1&lt;Microsoft.Management.Configuration.ConfigurationEnvironment&gt;" InterfaceId="47B18106-976B-5532-8E81-F58D304DFA43" /> <Interface Name="Windows.Foundation.Collections.IIterable`1&lt;Microsoft.Management.Configuration.IConfigurationUnitProcessorDetails&gt;" InterfaceId="055865E9-B633-5AD6-9C8F-55DFCD668E74" /> <Interface Name="Microsoft.Management.Configuration.IConfigurationUnitProcessorDetails2" InterfaceId="E89623ED-76E2-5145-B920-D09659554E35" /> + <Interface Name="Microsoft.Management.Configuration.IConfigurationUnitProcessorDetails3" InterfaceId="81511CCA-632B-560A-AFE8-D55555EB9937" /> <Interface Name="Microsoft.Management.Configuration.IGetAllSettingsConfigurationUnitProcessor" InterfaceId="72EB8304-D8D3-57D4-9940-7C1C4AD8C40C" /> <Interface Name="Microsoft.Management.Configuration.IGetAllUnitsConfigurationUnitProcessor" InterfaceId="D5CB3357-8AD6-5A3C-8695-057C01867D5F" /> <Interface Name="Microsoft.Management.Configuration.IFindUnitProcessorsSetProcessor" InterfaceId="620628DF-A5DE-591A-B738-FD8370B4E95C" /> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3319,4 +3319,30 @@ Please specify one of them using the --source option to proceed.</value> <data name="DscResourcePropertyDescriptionSourceExplicit" xml:space="preserve"> <value>Whether the source is included when calls don't specify a source.</value> </data> + <data name="ConfigurationApplyingUnit" xml:space="preserve"> + <value>Applying configuration unit...</value> + </data> + <data name="ConfigurationExportingUnit" xml:space="preserve"> + <value>Exporting configuration unit...</value> + </data> + <data name="ConfigurationGettingUnitProcessors" xml:space="preserve"> + <value>Getting configuration unit processors...</value> + </data> + <data name="ConfigurationExportInstallRequiredModule" xml:space="preserve"> + <value>Ensure required module for export [{0}]</value> + <comment>{Locked="{0}"}</comment> + </data> + <data name="ConfigurationExportInstallRequiredModuleFailed" xml:space="preserve"> + <value>Failed to test or acquire required module. Related settings will not be exported.</value> + </data> + <data name="ConfigurationExportUnitStart" xml:space="preserve"> + <value>Export [{0}]</value> + <comment>{Locked="{0}"}</comment> + </data> + <data name="ConfigurationExportUnitFailed" xml:space="preserve"> + <value>Failed to export the resource.</value> + </data> + <data name="ConfigurationExportFailedToGetUnitProcessors" xml:space="preserve"> + <value>Failed to get unit processors. Individual package settings will not be exported.</value> + </data> </root> diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -207,6 +207,18 @@ namespace AppInstaller::Repository::Microsoft index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledArchitecture, ToString(architecture.value())); } + + // May not be present on our oldest supported systems; simply ignore for the time being. + IPackage8 package8 = package.try_as<IPackage8>(); + if (package8) + { + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledLocation, + Utility::ConvertToUTF8(package8.InstalledPath())); + } + else + { + AICLI_LOG(Repo, Warning, << "Windows::ApplicationModel::Package::InstalledPath is not available on this version of Windows"); + } } } diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -120,6 +120,11 @@ namespace AppInstaller::Utility return a.length() >= b.length() && CaseInsensitiveEquals(a.substr(0, b.length()), b); } + bool CaseInsensitiveStartsWith(std::wstring_view a, std::wstring_view b) + { + return a.length() >= b.length() && CaseInsensitiveEquals(a.substr(0, b.length()), b); + } + bool CaseInsensitiveContainsSubstring(std::string_view a, std::string_view b) { auto it = std::search( @@ -998,4 +1003,21 @@ namespace AppInstaller::Utility return result; } + + std::string GetRandomString(size_t size) + { + static constexpr char chars[] = "0123456789abcdefghijklmnopqrstuvwxyz"; + static std::default_random_engine randomEngine(std::random_device{}()); + static std::uniform_int_distribution<long long> distribution(0, 35); + + std::string result; + result.resize(size); + + for (size_t i = 0; i < size; i++) + { + result[i] = chars[distribution(randomEngine)]; + } + + return result; + } } diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp @@ -327,6 +327,11 @@ namespace AppInstaller::Filesystem return Utility::ICUCaseInsensitiveEquals(Utility::ConvertToUTF8(volumeName1), Utility::ConvertToUTF8(volumeName2)); } + bool IsParentPath(const std::filesystem::path& path, const std::filesystem::path& parentPath) + { + return std::filesystem::weakly_canonical(path.parent_path()) == std::filesystem::weakly_canonical(parentPath); + } + void PathDetails::SetOwner(ACEPrincipal owner) { Owner = owner; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -115,10 +115,14 @@ namespace AppInstaller::Utility // Returns if a UTF8 string is contained within a vector in a case-insensitive manner. bool CaseInsensitiveContains(const std::vector<std::string_view>& a, std::string_view b); - // Determines if string a starts with string b. + // Determines if string a starts with string b. UTF8. // Use this if one of the values is a known value, and thus ToLower is sufficient. bool CaseInsensitiveStartsWith(std::string_view a, std::string_view b); + // Determines if string a starts with string b. UTF16. + // Use this if one of the values is a known value, and thus ToLower is sufficient. + bool CaseInsensitiveStartsWith(std::wstring_view a, std::wstring_view b); + // Determines if string a contains string b. // Use this if one of the values is a known value, and thus ToLower is sufficient. bool CaseInsensitiveContainsSubstring(std::string_view a, std::string_view b); @@ -292,4 +296,7 @@ namespace AppInstaller::Utility // 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); + + // Generates a random alpha numeric string. + std::string GetRandomString(size_t size = 8); } diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -49,6 +49,9 @@ namespace AppInstaller::Filesystem // Verifies that the paths are on the same volume. bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2); + // Verifies if 'path' has parent equal to 'parentPath' + bool IsParentPath(const std::filesystem::path& path, const std::filesystem::path& parentPath); + // The principal that an ACE applies to. enum class ACEPrincipal : uint32_t { diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h @@ -38,7 +38,8 @@ #include <memory> #include <mutex> #include <optional> -#include <ostream> +#include <ostream> +#include <random> #include <set> #include <string> #include <sstream> diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs @@ -130,12 +130,12 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers { if (this.resourceListItem != null) { - // TODO: Expose the Directory; requires adding a new property to the public interface result.UnitType = this.resourceListItem.Type; result.IsGroup = IsGroup(this.resourceListItem.Kind); result.Version = this.resourceListItem.Version; result.UnitDescription = this.resourceListItem.Description; result.Author = this.resourceListItem.Author; + result.Path = this.resourceListItem.Path; result.IsLocal = true; } diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs @@ -81,12 +81,7 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 } } - if (results.Count > 1) - { - throw new Exceptions.GetDscResourceMultipleMatches(resourceType, null); - } - - return results.FirstOrDefault(); + return this.GetResourceByLatestVersion(results); } /// <inheritdoc /> @@ -298,12 +293,34 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 this.RunSynchronously(processExecution); - if (processExecution.Output.Count > 1) + List<ResourceListItem> results = GetOutputLinesAs<ResourceListItem>(processExecution); + + return this.GetResourceByLatestVersion(results); + } + + private ResourceListItem? GetResourceByLatestVersion(List<ResourceListItem> resources) + { + // There may be different versions of same resource on the system. We check if all + // resource types match, we return the first one. + // TODO: May want to pick the latest one from the list. But since we are not using + // the version in our commands, picking any one is good for now. + ResourceListItem? candidate = null; + string candidateType = string.Empty; + + foreach (ResourceListItem resource in resources) { - throw new Exceptions.GetDscResourceMultipleMatches(resourceType, null); + if (candidate == null) + { + candidate = resource; + candidateType = resource.Type; + } + else if (!candidateType.Equals(resource.Type, StringComparison.OrdinalIgnoreCase)) + { + throw new Exceptions.GetDscResourceMultipleMatches(candidateType, null); + } } - return GetOptionalSingleOutputLineAs<ResourceListItem>(processExecution); + return candidate; } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs @@ -13,7 +13,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <summary> /// Provides information for a specific configuration unit within the runtime. /// </summary> - internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails, IConfigurationUnitProcessorDetails2 + internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails, IConfigurationUnitProcessorDetails2, IConfigurationUnitProcessorDetails3 { /// <summary> /// Initializes a new instance of the <see cref="ConfigurationUnitProcessorDetails"/> class. @@ -116,5 +116,10 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// Gets or sets a value indicating whether this resource is a group. /// </summary> public bool IsGroup { get; internal set; } + + /// <summary> + /// Gets or sets the path of the resource. + /// </summary> + public string? Path { get; internal set; } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -999,6 +999,72 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } + Configuration::ApplyConfigurationUnitResult ConfigurationProcessor::ApplyUnit(const ConfigurationUnit& unit) + { + THROW_HR_IF(E_NOT_VALID_STATE, !m_factory); + return ApplyUnitImpl(unit); + } + + Windows::Foundation::IAsyncOperation<Configuration::ApplyConfigurationUnitResult> ConfigurationProcessor::ApplyUnitAsync(const ConfigurationUnit& unit) + { + THROW_HR_IF(E_NOT_VALID_STATE, !m_factory); + + auto strong_this{ get_strong() }; + ConfigurationUnit localUnit = unit; + + co_await winrt::resume_background(); + + co_return ApplyUnitImpl(localUnit, { co_await winrt::get_cancellation_token() }); + } + + Configuration::ApplyConfigurationUnitResult ConfigurationProcessor::ApplyUnitImpl( + const ConfigurationUnit& unit, + AppInstaller::WinRT::AsyncCancellation cancellation) + { + auto threadGlobals = m_threadGlobals.SetForCurrentThread(); + + IConfigurationSetProcessor setProcessor = m_factory.CreateSetProcessor(nullptr); + auto result = make_self<wil::details::module_count_wrapper<implementation::ApplyConfigurationUnitResult>>(); + auto unitResult = make_self<wil::details::module_count_wrapper<implementation::ConfigurationUnitResultInformation>>(); + result->Unit(unit); + result->ResultInformation(*unitResult); + + cancellation.ThrowIfCancelled(); + + IConfigurationUnitProcessor unitProcessor; + + try + { + unitProcessor = setProcessor.CreateUnitProcessor(unit); + } + catch (...) + { + ExtractUnitResultInformation(std::current_exception(), unitResult); + } + + cancellation.ThrowIfCancelled(); + + if (unitProcessor) + { + try + { + auto applyResult = unitProcessor.ApplySettings(); + result->Unit(applyResult.Unit()); + result->State(Configuration::ConfigurationUnitState::Completed); + result->ResultInformation(applyResult.ResultInformation()); + result->RebootRequired(applyResult.RebootRequired()); + } + catch (...) + { + ExtractUnitResultInformation(std::current_exception(), unitResult); + } + + m_threadGlobals.GetTelemetryLogger().LogConfigUnitRunIfAppropriate(GUID_NULL, unit, ConfigurationUnitIntent::Apply, TelemetryTraceLogger::ApplyAction, result->ResultInformation()); + } + + return *result; + } + IConfigurationGroupProcessor ConfigurationProcessor::GetSetGroupProcessor(const Configuration::ConfigurationSet& configurationSet) { IConfigurationSetProcessor setProcessor = m_factory.CreateSetProcessor(configurationSet); diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -90,6 +90,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector<Configuration::IConfigurationUnitProcessorDetails> FindUnitProcessors(const Configuration::FindUnitProcessorsOptions& findOptions); Windows::Foundation::IAsyncOperation<Windows::Foundation::Collections::IVector<Configuration::IConfigurationUnitProcessorDetails>> FindUnitProcessorsAsync(const Configuration::FindUnitProcessorsOptions& findOptions); + Configuration::ApplyConfigurationUnitResult ApplyUnit(const ConfigurationUnit& unit); + Windows::Foundation::IAsyncOperation<Configuration::ApplyConfigurationUnitResult> ApplyUnitAsync(const ConfigurationUnit& unit); + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) @@ -131,6 +134,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector<Configuration::IConfigurationUnitProcessorDetails> FindUnitProcessorsImpl(const Configuration::FindUnitProcessorsOptions& findOptions, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Configuration::ApplyConfigurationUnitResult ApplyUnitImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + IConfigurationGroupProcessor GetSetGroupProcessor(const Configuration::ConfigurationSet& configurationSet); void SendDiagnosticsImpl(const IDiagnosticInformation& information); diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl @@ -205,6 +205,14 @@ namespace Microsoft.Management.Configuration Boolean IsGroup{ get; }; } + // Provides information for a specific configuration unit within the runtime. + [contract(Microsoft.Management.Configuration.Contract, 4)] + interface IConfigurationUnitProcessorDetails3 requires IConfigurationUnitProcessorDetails2 + { + // The path of the resource. + String Path{ get; }; + } + // Defines how the configuration unit is to be used within the configuration system. [contract(Microsoft.Management.Configuration.Contract, 1)] enum ConfigurationUnitIntent @@ -1016,6 +1024,10 @@ namespace Microsoft.Management.Configuration // Find unit processors. Windows.Foundation.Collections.IVector<IConfigurationUnitProcessorDetails> FindUnitProcessors(FindUnitProcessorsOptions findOptions); Windows.Foundation.IAsyncOperation< Windows.Foundation.Collections.IVector<IConfigurationUnitProcessorDetails> > FindUnitProcessorsAsync(FindUnitProcessorsOptions findOptions); + + // Apply the current configuration unit. + ApplyConfigurationUnitResult ApplyUnit(ConfigurationUnit unit); + Windows.Foundation.IAsyncOperation<ApplyConfigurationUnitResult> ApplyUnitAsync(ConfigurationUnit unit); } }