winget-cli

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

commit fb3650a4328e5489fca689e1885e47a855e37e9c
parent d637f0e86f1f96b3dbf9b1e96892538ab2063ba8
Author: Ryan <69221034+ryfu-msft@users.noreply.github.com>
Date:   Fri, 14 Jul 2023 19:16:49 -0700

Download command (#3376)


Diffstat:
Mdoc/Settings.md | 11+++++++++++
Mdoc/windows/package-manager/winget/returnCodes.md | 1+
Mschemas/JSON/settings/settings.schema.0.2.json | 18+++++++++++++++++-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Msrc/AppInstallerCLICore/Argument.cpp | 13+++++++++++--
Msrc/AppInstallerCLICore/Command.cpp | 9+++++++++
Msrc/AppInstallerCLICore/Commands/COMCommand.cpp | 7+++++--
Asrc/AppInstallerCLICore/Commands/DownloadCommand.cpp | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/DownloadCommand.h | 23+++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 4++--
Msrc/AppInstallerCLICore/Commands/RootCommand.cpp | 3+++
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 4++--
Msrc/AppInstallerCLICore/ContextOrchestrator.cpp | 8++++++++
Msrc/AppInstallerCLICore/ContextOrchestrator.h | 3+++
Msrc/AppInstallerCLICore/ExecutionArgs.h | 4++++
Msrc/AppInstallerCLICore/ExecutionContext.h | 1+
Msrc/AppInstallerCLICore/ExecutionContextData.h | 7+++++++
Msrc/AppInstallerCLICore/Resources.h | 13++++++++++---
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 25++++++++++++++-----------
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.h | 10++++++----
Msrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp | 1-
Msrc/AppInstallerCLICore/Workflows/DownloadFlow.cpp | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/AppInstallerCLICore/Workflows/DownloadFlow.h | 6++++++
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 105++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.h | 26++++++++++++++++----------
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.cpp | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.h | 1+
Msrc/AppInstallerCLICore/Workflows/PromptFlow.cpp | 21++++++++++++++++-----
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 4++--
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 1+
Msrc/AppInstallerCLIE2ETests/Constants.cs | 5+++++
Asrc/AppInstallerCLIE2ETests/DownloadCommand.cs | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/FeaturesCommand.cs | 2++
Asrc/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs | 7++++++-
Msrc/AppInstallerCLIE2ETests/SetUpFixture.cs | 1-
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml | 39+++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestHashHelper.cs | 9++++++---
Msrc/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs | 2++
Msrc/AppInstallerCLIPackage/Package.appxmanifest | 2++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 30++++++++++++++++++++++++++----
Msrc/AppInstallerCLITests/InstallDependenciesFlow.cpp | 27+++++++++++++--------------
Msrc/AppInstallerCLITests/UpdateFlow.cpp | 2+-
Msrc/AppInstallerCLITests/UserSettings.cpp | 15+++++++++++++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/AppInstallerRuntime.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/DependenciesGraph.h | 2+-
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 7+++++++
Msrc/AppInstallerCommonCore/Runtime.cpp | 10++++++----
Msrc/AppInstallerCommonCore/UserSettings.cpp | 6++++++
Msrc/AppInstallerSharedLib/Errors.cpp | 2++
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 1+
Msrc/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest | 4++++
Msrc/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs | 12++++++++++++
Msrc/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs | 2++
Msrc/Microsoft.Management.Deployment/ComClsids.cpp | 5+++++
Msrc/Microsoft.Management.Deployment/Converters.cpp | 31+++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/Converters.h | 19++++++++++++-------
Asrc/Microsoft.Management.Deployment/DownloadOptions.cpp | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/DownloadOptions.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/DownloadResult.cpp | 31+++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/DownloadResult.h | 30++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj | 4++++
Msrc/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters | 30+++++++++++++++++-------------
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/Microsoft.Management.Deployment/PackageManager.h | 5+++++
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/Public/ComClsids.h | 3+++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ComObjectFactory.cs | 14++++++++++++++
Msrc/WindowsPackageManager/main.cpp | 1+
75 files changed, 1878 insertions(+), 136 deletions(-)

diff --git a/doc/Settings.md b/doc/Settings.md @@ -281,4 +281,15 @@ You can enable the feature as shown below. "experimentalFeatures": { "windowsFeature": true }, +``` + +### download + +This feature enables the download command. This command allows users to download the installers of a specified package. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "download": true + }, ``` \ No newline at end of file diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -118,6 +118,7 @@ ms.localizationpriority: medium | 0x8A150068 | -1978335128 | APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED | The package has a pin that prevents upgrade. | | 0x8A150069 | -1978335127 | APPINSTALLER_CLI_ERROR_PACKAGE_IS_STUB | The package currently installed is the stub package | | 0x8A15006A | -1978335126 | APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED | Application shutdown signal received | +| 0x8A15006B | -1978335125 | APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES | Failed to download package dependencies. | ## Install errors. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -136,7 +136,18 @@ "purgePortablePackage": { "description": "Controls whether the default behavior for uninstall removes all files and directories relevant to this package. Only applies to the portable installerType.", "type": "boolean", - "default": false + "default": false + } + } + }, + "DownloadBehavior": { + "description": "Download settings", + "type": "object", + "properties": { + "defaultDownloadDirectory": { + "description": "The default directory where installers are downloaded to.", + "type": "string", + "default": "%USERPROFILE%/Downloads/" } } }, @@ -213,6 +224,11 @@ "description": "Enable support for configuration", "type": "boolean", "default": false + }, + "download": { + "description": "Enable support for the download command", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -358,6 +358,7 @@ <ClInclude Include="Commands\ConfigureTestCommand.h" /> <ClInclude Include="Commands\ConfigureValidateCommand.h" /> <ClInclude Include="Commands\DebugCommand.h" /> + <ClInclude Include="Commands\DownloadCommand.h" /> <ClInclude Include="Commands\ExperimentalCommand.h" /> <ClInclude Include="Commands\ExportCommand.h" /> <ClInclude Include="Commands\ImportCommand.h" /> @@ -425,6 +426,7 @@ <ClCompile Include="Commands\ConfigureTestCommand.cpp" /> <ClCompile Include="Commands\ConfigureValidateCommand.cpp" /> <ClCompile Include="Commands\DebugCommand.cpp" /> + <ClCompile Include="Commands\DownloadCommand.cpp" /> <ClCompile Include="Commands\ImportCommand.cpp" /> <ClCompile Include="Commands\PinCommand.cpp" /> <ClCompile Include="Commands\TestCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -224,6 +224,9 @@ <ClInclude Include="Commands\TestCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Commands\DownloadCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -415,6 +418,9 @@ <ClCompile Include="Commands\TestCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClInclude Include="Commands\DownloadCommand.cpp"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -77,6 +77,8 @@ namespace AppInstaller::CLI return { type, "scope"_liv, ArgTypeCategory::InstallerSelection | ArgTypeCategory::CopyValueToSubContext }; case Execution::Args::Type::InstallArchitecture: return { type, "architecture"_liv, 'a', ArgTypeCategory::InstallerSelection | ArgTypeCategory::CopyValueToSubContext }; + case Execution::Args::Type::InstallerType: + return { type, "installer-type"_liv, ArgTypeCategory::InstallerSelection }; case Execution::Args::Type::HashOverride: return { type, "ignore-security-hash"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext }; case Execution::Args::Type::IgnoreLocalArchiveMalwareScan: @@ -164,7 +166,6 @@ namespace AppInstaller::CLI case Execution::Args::Type::Upgrade: return { type, "upgrade-available"_liv}; - // Pin command case Execution::Args::Type::GatedVersion: return { type, "version"_liv, 'v', ArgTypeCategory::None, ArgTypeExclusiveSet::PinType }; @@ -183,6 +184,10 @@ namespace AppInstaller::CLI case Execution::Args::Type::ConfigurationDisable: return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; + // Download command + case Execution::Args::Type::DownloadDirectory: + return { type, "download-directory"_liv, 'd', ArgTypeCategory::None }; + // Common arguments case Execution::Args::Type::NoVT: return { type, "no-vt"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::ProgressBarOption }; @@ -271,7 +276,7 @@ namespace AppInstaller::CLI case Args::Type::Locale: return Argument{ type, Resource::String::LocaleArgumentDescription, ArgumentType::Standard }; case Args::Type::InstallArchitecture: - return Argument{ type, Resource::String::InstallArchitectureArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; + return Argument{ type, Resource::String::ArchitectureArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::Log: return Argument{ type, Resource::String::LogArgumentDescription, ArgumentType::Standard }; case Args::Type::CustomSwitches: @@ -336,6 +341,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::UninstallPreviousArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }; case Args::Type::Force: return Argument{ type, Resource::String::ForceArgumentDescription, ArgumentType::Flag, false }; + case Args::Type::DownloadDirectory: + return Argument{ type, Resource::String::DownloadDirectoryArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; + case Args::Type::InstallerType: + return Argument{ type, Resource::String::InstallerTypeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -720,6 +720,15 @@ namespace AppInstaller::CLI } } + if (execArgs.Contains(Execution::Args::Type::InstallerType)) + { + Manifest::InstallerTypeEnum selectedInstallerType = Manifest::ConvertToInstallerTypeEnum(std::string(execArgs.GetArg(Execution::Args::Type::InstallerType))); + if (selectedInstallerType == Manifest::InstallerTypeEnum::Unknown) + { + throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues(Argument::ForType(Execution::Args::Type::InstallerType).Name())); + } + } + Argument::ValidateExclusiveArguments(execArgs); ValidateArgumentsInternal(execArgs); diff --git a/src/AppInstallerCLICore/Commands/COMCommand.cpp b/src/AppInstallerCLICore/Commands/COMCommand.cpp @@ -7,6 +7,7 @@ #include "Workflows/PromptFlow.h" #include "Workflows/UninstallFlow.h" #include "Workflows/WorkflowBase.h" +#include "Workflows/DependenciesFlow.h" namespace AppInstaller::CLI { @@ -24,7 +25,8 @@ namespace AppInstaller::CLI Workflow::EnsureApplicableInstaller << Workflow::ReportIdentityAndInstallationDisclaimer << Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << - Workflow::ManageDependencies << // TODO: Separate handling dependencies from download flow. + Workflow::SetDownloadDirectory << + Workflow::DownloadPackageDependencies << Workflow::DownloadInstaller; } @@ -32,7 +34,8 @@ namespace AppInstaller::CLI void COMInstallCommand::ExecuteInternal(Context& context) const { context << - Workflow::ReverifyInstallerHash << + Workflow::InstallDependencies << + Workflow::ReverifyInstallerHash << Workflow::InstallPackageInstaller; } diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DownloadCommand.h" +#include "Workflows/DownloadFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/PromptFlow.h" +#include "Resources.h" +#include <AppInstallerRuntime.h> + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::CLI::Execution; + using namespace AppInstaller::CLI::Workflow; + using namespace AppInstaller::Utility::literals; + + std::vector<Argument> DownloadCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::DownloadDirectory), + Argument::ForType(Args::Type::Manifest), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Moniker), + Argument::ForType(Args::Type::Version), + Argument::ForType(Args::Type::Channel), + Argument::ForType(Args::Type::Source), + Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument::ForType(Args::Type::InstallArchitecture), + Argument::ForType(Args::Type::InstallerType), + Argument::ForType(Args::Type::Exact), + Argument::ForType(Args::Type::Locale), + Argument::ForType(Args::Type::HashOverride), + Argument::ForType(Args::Type::SkipDependencies), + Argument::ForType(Execution::Args::Type::AcceptPackageAgreements), + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), + }; + } + + Resource::LocString DownloadCommand::ShortDescription() const + { + return { Resource::String::DownloadCommandShortDescription }; + } + + Resource::LocString DownloadCommand::LongDescription() const + { + return { Resource::String::DownloadCommandLongDescription }; + } + + Utility::LocIndView DownloadCommand::HelpLink() const + { + return "https://aka.ms/winget-command-download"_liv; + } + + void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const + { + Argument::ValidateCommonArguments(execArgs); + } + + void DownloadCommand::ExecuteInternal(Context& context) const + { + context.SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly); + + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::GetManifestFromArg; + } + else + { + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::OpenSource() << + Workflow::SearchSourceForSingle << + Workflow::HandleSearchResultFailures << + Workflow::EnsureOneMatchFromSearchResult(OperationType::Download) << + Workflow::GetManifestFromPackage(false); + } + + context << + Workflow::SetDownloadDirectory << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << + Workflow::DownloadPackageDependencies << + Workflow::DownloadInstaller; + } +} diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.h b/src/AppInstallerCLICore/Commands/DownloadCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct DownloadCommand final : public Command + { + DownloadCommand(std::string_view parent) : Command("download", {} /* aliases */, parent, Settings::ExperimentalFeature::Feature::Download) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -132,8 +132,8 @@ namespace AppInstaller::CLI Workflow::GetMultiSearchRequests << Workflow::SearchSubContextsForSingle() << Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << - Workflow::InstallMultiplePackages( - Resource::String::InstallAndUpgradeCommandsReportDependencies, + Workflow::ProcessMultiplePackages( + Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_MULTIPLE_INSTALL_FAILED); } else diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -23,6 +23,7 @@ #include "ConfigureCommand.h" #include "DebugCommand.h" #include "TestCommand.h" +#include "DownloadCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -140,6 +141,7 @@ namespace AppInstaller::CLI keyDirectories.OutputLine({ Resource::LocString{ Resource::String::PortableRootUser }, Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot, true).u8string() }); keyDirectories.OutputLine({ Resource::LocString{ Resource::String::PortableRoot }, Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRoot, true).u8string() }); keyDirectories.OutputLine({ Resource::LocString{ Resource::String::PortableRoot86 }, Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86, true).u8string() }); + keyDirectories.OutputLine({ Resource::LocString{ Resource::String::InstallerDownloads }, Runtime::GetPathTo(Runtime::PathName::UserProfileDownloads, true).u8string() }); keyDirectories.Complete(); context.Reporter.Info() << std::endl; } @@ -177,6 +179,7 @@ namespace AppInstaller::CLI std::make_unique<ImportCommand>(FullName()), std::make_unique<PinCommand>(FullName()), std::make_unique<ConfigureCommand>(FullName()), + std::make_unique<DownloadCommand>(FullName()), #if _DEBUG std::make_unique<DebugCommand>(FullName()), #endif diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -202,8 +202,8 @@ namespace AppInstaller::CLI Workflow::GetMultiSearchRequests << Workflow::SearchSubContextsForSingle(OperationType::Upgrade) << Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << - Workflow::InstallMultiplePackages( - Resource::String::InstallAndUpgradeCommandsReportDependencies, + Workflow::ProcessMultiplePackages( + Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_MULTIPLE_INSTALL_FAILED); } } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.cpp b/src/AppInstallerCLICore/ContextOrchestrator.cpp @@ -362,6 +362,7 @@ namespace AppInstaller::CLI::Execution case PackageOperationType::Install: return "root:install"sv; case PackageOperationType::Upgrade: return "root:upgrade"sv; case PackageOperationType::Uninstall: return "root:uninstall"sv; + case PackageOperationType::Download: return "root:download"sv; default: return "unknown"; } } @@ -386,4 +387,11 @@ namespace AppInstaller::CLI::Execution std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Search); return item; } + + std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForDownload(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) + { + std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Download); + item->AddCommand(std::make_unique<::AppInstaller::CLI::COMDownloadCommand>(RootCommand::CommandName)); + return item; + } } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.h b/src/AppInstallerCLICore/ContextOrchestrator.h @@ -46,6 +46,7 @@ namespace AppInstaller::CLI::Execution Install, Upgrade, Uninstall, + Download, }; struct OrchestratorQueueItem @@ -98,6 +99,8 @@ namespace AppInstaller::CLI::Execution static std::unique_ptr<OrchestratorQueueItem> CreateItemForUninstall(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); // Create queue item for finding existing entry from the orchestrator queue static std::unique_ptr<OrchestratorQueueItem> CreateItemForSearch(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); + // Create queue item for download + static std::unique_ptr<OrchestratorQueueItem> CreateItemForDownload(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); }; struct ContextOrchestrator diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -41,6 +41,7 @@ namespace AppInstaller::CLI::Execution InstallLocation, InstallScope, InstallArchitecture, + InstallerType, HashOverride, // Ignore hash mismatches SkipDependencies, // Skip dependencies IgnoreLocalArchiveMalwareScan, // Ignore the local malware scan on archive files @@ -80,6 +81,9 @@ namespace AppInstaller::CLI::Execution IgnoreUnavailable, IgnoreVersions, + // Download Command + DownloadDirectory, + // Setting Command AdminSettingEnable, AdminSettingDisable, diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -65,6 +65,7 @@ namespace AppInstaller::CLI::Execution ShowSearchResultsOnPartialFailure = 0x10, DisableInteractivity = 0x40, BypassIsStoreClientBlockedPolicyCheck = 0x80, + InstallerDownloadOnly = 0x100, }; DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -66,6 +66,7 @@ namespace AppInstaller::CLI::Execution PinningIndex, Pins, ConfigurationContext, + DownloadDirectory, Max }; @@ -264,5 +265,11 @@ namespace AppInstaller::CLI::Execution { using value_t = ConfigurationContext; }; + + template <> + struct DataMapping<Data::DownloadDirectory> + { + using value_t = std::filesystem::path; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -28,6 +28,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnabled); WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnableDescription); WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingHeader); + WINGET_DEFINE_RESOURCE_STRINGID(ArchitectureArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScan); WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScanOverridden); WINGET_DEFINE_RESOURCE_STRINGID(ArgumentForSinglePackageProvidedWithMultipleQueries); @@ -122,6 +123,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(CustomSwitchesArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowDownload); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowInstall); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceNotFound); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceTooManyMatches); @@ -138,6 +140,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DisableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(DisableInteractivityArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); + WINGET_DEFINE_RESOURCE_STRINGID(DownloadCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DownloadCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DownloadDirectoryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Downloading); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature); @@ -202,8 +207,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(IncludePinnedArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(IncompatibleArgumentsProvided); - WINGET_DEFINE_RESOURCE_STRINGID(InstallAndUpgradeCommandsReportDependencies); - WINGET_DEFINE_RESOURCE_STRINGID(InstallArchitectureArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallationAbandoned); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); @@ -214,8 +217,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageVersionNotAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstalledScopeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallerAbortsTerminal); - WINGET_DEFINE_RESOURCE_STRINGID(InstallerElevationExpected); WINGET_DEFINE_RESOURCE_STRINGID(InstallerBlockedByPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloaded); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloads); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerElevationExpected); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedVirusScan); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedWithCode); @@ -227,6 +232,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallerLogAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstallerProhibitsElevation); WINGET_DEFINE_RESOURCE_STRINGID(InstallerRequiresInstallLocation); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerTypeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallersAbortTerminal); WINGET_DEFINE_RESOURCE_STRINGID(InstallersRequireInstallLocation); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); @@ -325,6 +331,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(PackageDependencies); WINGET_DEFINE_RESOURCE_STRINGID(PackageIsPinned); + WINGET_DEFINE_RESOURCE_STRINGID(PackageRequiresDependencies); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); WINGET_DEFINE_RESOURCE_STRINGID(PinAddBlockingArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -123,14 +123,18 @@ namespace AppInstaller::CLI::Workflow { const auto& packageVersion = context.Get<Execution::Data::PackageVersion>(); context.Add<Execution::Data::DependencySource>(packageVersion->GetSource()); - context << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true, Repository::CompositeSearchBehavior::AvailablePackages); } else { // install from manifest requires --dependency-source to be set context << - Workflow::OpenSource(true) << + Workflow::OpenSource(true); + } + + if (WI_IsFlagClear(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) + { + // Installed source is not needed when only downloading the installer. + context << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true, Repository::CompositeSearchBehavior::AvailablePackages); } } @@ -233,7 +237,7 @@ namespace AppInstaller::CLI::Workflow } } - void ManagePackageDependencies::operator()(Execution::Context& context) const + void CreateDependencySubContexts::operator()(Execution::Context& context) const { if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { @@ -280,6 +284,7 @@ namespace AppInstaller::CLI::Workflow std::move(nodeProcessor.GetPackageInstalledVersion()), std::move(nodeProcessor.GetManifest()), std::move(nodeProcessor.GetPreferredInstaller()) }; + idToPackageMap.emplace(node.Id(), std::move(dependencyPackageCandidate)); }; @@ -332,17 +337,15 @@ namespace AppInstaller::CLI::Workflow dependencyContext.Add<Execution::Data::InstalledPackageVersion>(itr->second.InstalledPackageVersion); dependencyContext.Add<Execution::Data::Installer>(itr->second.Installer); + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) + { + dependencyContext.Add<Execution::Data::DownloadDirectory>(context.Get<Execution::Data::DownloadDirectory>() / "Dependencies"); + } + dependencyPackageContexts.emplace_back(std::move(dependencyContextPtr)); } } - if (!dependencyPackageContexts.empty()) - { - info << Resource::String::DependenciesFlowInstall << std::endl; - } - - // Install dependencies in the correct order context.Add<Execution::Data::PackageSubContexts>(std::move(dependencyPackageContexts)); - context << Workflow::InstallMultiplePackages(m_dependencyReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, true, true, true); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.h b/src/AppInstallerCLICore/Workflows/DependenciesFlow.h @@ -39,14 +39,16 @@ namespace AppInstaller::CLI::Workflow // Outputs: Dependencies void GetDependenciesInfoForUninstall(Execution::Context& context); - // Builds the dependency graph. + // Builds the dependency graph and creates the sub contexts for each package dependency. // Required Args: None // Inputs: Manifest, Installer and DependencySource // Outputs: Dependencies - struct ManagePackageDependencies : public WorkflowTask + struct CreateDependencySubContexts : public WorkflowTask { - ManagePackageDependencies(AppInstaller::StringResource::StringId dependencyReportMessage) : - WorkflowTask("ReportDependencies"), m_dependencyReportMessage(dependencyReportMessage) {} + CreateDependencySubContexts( + AppInstaller::StringResource::StringId dependencyReportMessage) : + WorkflowTask("CreateDependencySubContexts"), + m_dependencyReportMessage(dependencyReportMessage) {} void operator()(Execution::Context& context) const override; diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -60,7 +60,6 @@ namespace AppInstaller::CLI::Workflow // as we won't keep searching for dependencies for installed packages return DependencyNodeProcessorResult::Skipped; } - if (!m_nodePackageLatestVersion) { diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -89,6 +89,44 @@ namespace AppInstaller::CLI::Workflow return filename; } + // Gets the file name for the downloaded installer in the format of {id}_{version}_{architecture}_{scope}_{installerType}_{locale}. + std::filesystem::path GetInstallerDownloadOnlyFileName(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + const auto& installer = context.Get<Execution::Data::Installer>().value(); + + std::string packageName = manifest.CurrentLocalization.Get<Localization::PackageName>(); + std::string architecture{ ToString(installer.Arch) }; + std::string installerType{ InstallerTypeToString(installer.EffectiveInstallerType()) }; + + std::string fileName = packageName; + + if (!Version(manifest.Version).IsUnknown()) + { + fileName += '_' + manifest.Version; + } + + if (installer.Scope != ScopeEnum::Unknown) + { + fileName += '_' + std::string{ ScopeToString(installer.Scope) }; + } + + fileName += '_' + architecture + '_' + installerType; + + std::string locale = !installer.Locale.empty() ? installer.Locale : manifest.CurrentLocalization.Locale; + if (!locale.empty()) + { + fileName += '_' + locale; + } + + std::filesystem::path fileNamePath = Utility::ConvertToUTF16(fileName); + fileNamePath += GetInstallerFileExtension(context); + + // Make file name suitable for file system path + fileNamePath = Utility::ConvertToUTF16(Utility::MakeSuitablePathPart(fileNamePath.u8string())); + return fileNamePath; + } + // Try to remove the installer file, ignoring any errors. void RemoveInstallerFile(const std::filesystem::path& path) { @@ -139,6 +177,7 @@ namespace AppInstaller::CLI::Workflow context << ReportExecutionStage(ExecutionStage::Download) << CheckForExistingInstaller; + if (context.IsTerminated()) { return; @@ -161,8 +200,9 @@ namespace AppInstaller::CLI::Workflow context << DownloadInstallerFile; break; case InstallerTypeEnum::Msix: - if (installer.SignatureSha256.empty()) + if (installer.SignatureSha256.empty() || WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) { + // If InstallerDownloadOnly flag is set, always download the installer file. context << DownloadInstallerFile; } else @@ -172,8 +212,15 @@ namespace AppInstaller::CLI::Workflow } break; case InstallerTypeEnum::MSStore: - // Nothing to do here - return; + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + else + { + // Nothing to do here + return; + } default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -441,16 +488,40 @@ namespace AppInstaller::CLI::Workflow } auto& installerPath = context.Get<Execution::Data::InstallerPath>(); - std::filesystem::path renamedDownloadedInstaller = installerPath; - renamedDownloadedInstaller.replace_filename(GetInstallerPostHashValidationFileName(context)); + std::filesystem::path renamedDownloadedInstaller; - if (installerPath == renamedDownloadedInstaller) + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) { - // In case we are reusing an existing downloaded file - return; + THROW_HR_IF(E_UNEXPECTED, !context.Contains(Execution::Data::DownloadDirectory)); + + std::filesystem::path downloadDirectory = context.Get<Execution::Data::DownloadDirectory>(); + + if (!std::filesystem::exists(downloadDirectory)) + { + std::filesystem::create_directories(downloadDirectory); + } + else + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANNOT_MAKE), !std::filesystem::is_directory(downloadDirectory)); + } + + renamedDownloadedInstaller = downloadDirectory / GetInstallerDownloadOnlyFileName(context); + Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); + context.Reporter.Info() << Resource::String::InstallerDownloaded(Utility::LocIndView{ renamedDownloadedInstaller.u8string() }) << std::endl; } + else + { + renamedDownloadedInstaller = installerPath; + renamedDownloadedInstaller.replace_filename(GetInstallerPostHashValidationFileName(context)); - Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); + if (installerPath == renamedDownloadedInstaller) + { + // In case we are reusing an existing downloaded file + return; + } + + Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); + } installerPath.assign(renamedDownloadedInstaller); AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); @@ -466,4 +537,30 @@ namespace AppInstaller::CLI::Workflow RemoveInstallerFile(path); } } + + void SetDownloadDirectory(Execution::Context& context) + { + if (!WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) + { + return; + } + + if (context.Args.Contains(Execution::Args::Type::DownloadDirectory)) + { + context.Add<Execution::Data::DownloadDirectory>(std::filesystem::path{ context.Args.GetArg(Execution::Args::Type::DownloadDirectory) }); + } + else + { + std::filesystem::path downloadsDirectory = Settings::User().Get<Settings::Setting::DownloadDefaultDirectory>(); + + if (downloadsDirectory.empty()) + { + downloadsDirectory = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::UserProfileDownloads); + } + + const auto& manifest = context.Get<Execution::Data::Manifest>(); + std::string packageDownloadFolderName = manifest.Id + '_' + manifest.Version; + context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / packageDownloadFolderName); + } + } } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.h b/src/AppInstallerCLICore/Workflows/DownloadFlow.h @@ -66,4 +66,10 @@ namespace AppInstaller::CLI::Workflow // Inputs: InstallerPath // Outputs: None void RemoveInstaller(Execution::Context& context); + + // Sets the target download directory location if applicable. + // Required Args: None + // Inputs: Manifest + // Outputs: None + void SetDownloadDirectory(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -305,7 +305,7 @@ namespace AppInstaller::CLI::Workflow void InstallImportedPackages(Execution::Context& context) { - context << Workflow::InstallMultiplePackages( + context << Workflow::ProcessMultiplePackages( Resource::String::ImportCommandReportDependencies, APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED, {}, true, true); if (context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -293,14 +293,6 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } - // This installer cannot be run elevated, but we are running elevated. - // Implementation of de-elevation is complex; simply block for now. - if (installer->ElevationRequirement == ElevationRequirementEnum::ElevationProhibited && Runtime::IsRunningAsAdmin()) - { - context.Reporter.Error() << Resource::String::InstallerProhibitsElevation << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION); - } - context << EnsureSupportForInstall; } @@ -541,7 +533,7 @@ namespace AppInstaller::CLI::Workflow Workflow::DisplayInstallationNotes; } - void ManageDependencies(Execution::Context& context) + void InstallDependencies(Execution::Context& context) { if (Settings::User().Get<Settings::Setting::InstallSkipDependencies>() || context.Args.Contains(Execution::Args::Type::SkipDependencies)) { @@ -551,9 +543,24 @@ namespace AppInstaller::CLI::Workflow context << Workflow::GetDependenciesFromInstaller << - Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + Workflow::ReportDependencies(Resource::String::PackageRequiresDependencies) << Workflow::EnableWindowsFeaturesDependencies << - Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + Workflow::ProcessMultiplePackages(Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, true, true, true); + } + + void DownloadPackageDependencies(Execution::Context& context) + { + if (Settings::User().Get<Settings::Setting::InstallSkipDependencies>() || context.Args.Contains(Execution::Args::Type::SkipDependencies)) + { + context.Reporter.Warn() << Resource::String::DependenciesSkippedMessage << std::endl; + return; + } + + context << + Workflow::GetDependenciesFromInstaller << + Workflow::ReportDependencies(Resource::String::PackageRequiresDependencies) << + Workflow::CreateDependencySubContexts(Resource::String::PackageRequiresDependencies) << + Workflow::ProcessMultiplePackages(Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES, {}, true, true, true, false); } void InstallSinglePackage(Execution::Context& context) @@ -562,21 +569,42 @@ namespace AppInstaller::CLI::Workflow Workflow::CheckForUnsupportedArgs << Workflow::ReportIdentityAndInstallationDisclaimer << Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << - Workflow::ManageDependencies << + Workflow::CreateDependencySubContexts(Resource::String::PackageRequiresDependencies) << + Workflow::InstallDependencies << Workflow::DownloadInstaller << Workflow::InstallPackageInstaller; } void EnsureSupportForInstall(Execution::Context& context) { + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) + { + return; + } + + const auto& installer = context.Get<Execution::Data::Installer>(); + + // This installer cannot be run elevated, but we are running elevated. + // Implementation of de-elevation is complex; simply block for now. + if (installer->ElevationRequirement == ElevationRequirementEnum::ElevationProhibited && Runtime::IsRunningAsAdmin()) + { + context.Reporter.Error() << Resource::String::InstallerProhibitsElevation << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION); + } + context << Workflow::EnsureRunningAsAdminForMachineScopeInstall << Workflow::EnsureSupportForPortableInstall << Workflow::EnsureValidNestedInstallerMetadataForArchiveInstall; } - void InstallMultiplePackages::operator()(Execution::Context& context) const + void ProcessMultiplePackages::operator()(Execution::Context& context) const { + if (!context.Contains(Execution::Data::PackageSubContexts)) + { + return; + } + // Show all prompts needed for every package before installing anything context << Workflow::ShowPromptsForMultiplePackages(m_ensurePackageAgreements); @@ -585,11 +613,27 @@ namespace AppInstaller::CLI::Workflow return; } + bool downloadInstallerOnly = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly); + // Report dependencies if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { + auto& packageSubContexts = context.Get<Execution::Data::PackageSubContexts>(); + if (!packageSubContexts.empty()) + { + if (downloadInstallerOnly) + { + context.Reporter.Info() << Resource::String::DependenciesFlowDownload << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::DependenciesFlowInstall << std::endl; + } + } + DependencyList allDependencies; - for (auto& packageContext : context.Get<Execution::Data::PackageSubContexts>()) + + for (auto& packageContext : packageSubContexts) { allDependencies.Add(packageContext->Get<Execution::Data::Installer>().value().Dependencies); } @@ -608,25 +652,36 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << '(' << packagesProgress << '/' << packagesCount << ") "_liv; // We want to do best effort to install all packages regardless of previous failures - Execution::Context& installContext = *packageContext; - auto previousThreadGlobals = installContext.SetForCurrentThread(); + Execution::Context& currentContext = *packageContext; + auto previousThreadGlobals = currentContext.SetForCurrentThread(); - installContext << Workflow::ReportIdentityAndInstallationDisclaimer; + currentContext << Workflow::ReportIdentityAndInstallationDisclaimer; // Prevent individual exceptions from breaking out of the loop try { if (!m_ignorePackageDependencies) { - installContext << Workflow::ManagePackageDependencies(m_dependenciesReportMessage); + if (!downloadInstallerOnly) + { + currentContext << Workflow::EnableWindowsFeaturesDependencies; + } + + currentContext << + Workflow::CreateDependencySubContexts(m_dependenciesReportMessage) << + Workflow::ProcessMultiplePackages(m_dependenciesReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, true, true, true, true); + } + + currentContext << Workflow::DownloadInstaller; + + if (!downloadInstallerOnly) + { + currentContext << Workflow::InstallPackageInstaller; } - installContext << - Workflow::DownloadInstaller << - Workflow::InstallPackageInstaller; } catch (...) { - installContext.SetTerminationHR(Workflow::HandleException(installContext, std::current_exception())); + currentContext.SetTerminationHR(Workflow::HandleException(currentContext, std::current_exception())); } if (m_refreshPathVariable) @@ -642,9 +697,9 @@ namespace AppInstaller::CLI::Workflow } } - installContext.Reporter.Info() << std::endl; + currentContext.Reporter.Info() << std::endl; - if (installContext.IsTerminated()) + if (currentContext.IsTerminated()) { if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) { @@ -653,7 +708,7 @@ namespace AppInstaller::CLI::Workflow return; } - if (m_ignorableInstallResults.end() == std::find(m_ignorableInstallResults.begin(), m_ignorableInstallResults.end(), installContext.GetTerminationHR())) + if (m_ignorableInstallResults.end() == std::find(m_ignorableInstallResults.begin(), m_ignorableInstallResults.end(), currentContext.GetTerminationHR())) { allSucceeded = false; if (m_stopOnFailure) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -136,17 +136,23 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportIdentityAndInstallationDisclaimer(Execution::Context& context); - // Installs a specific package installer. See also InstallSinglePackage & InstallMultiplePackages. + // Installs a specific package installer. See also InstallSinglePackage & ProcessMultiplePackages // Required Args: None // Inputs: InstallerPath, Manifest, Installer, PackageVersion, InstalledPackageVersion? // Outputs: None void InstallPackageInstaller(Execution::Context& context); - - // Manages the dependencies for a single package. + + // Installs the dependencies for a specific package. // Required Args: None + // Inputs: InstallerPath, Manifest, Installer, PackageVersion, InstalledPackageVersion? + // Outputs: None + void InstallDependencies(Execution::Context& context); + + // Downloads all of the package dependencies of a specific package. Only used in the 'winget download' and COM download flows. + // Required Args: none // Inputs: Manifest, Installer - // Outputs: InstallerPath - void ManageDependencies(Execution::Context& context); + // Outputs: None + void DownloadPackageDependencies(Execution::Context& context); // Installs a single package. This also does the reporting, user interaction, and installer download // for single-package installation. @@ -155,21 +161,21 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void InstallSinglePackage(Execution::Context& context); - // Installs multiple packages. This also does the reporting and user interaction needed. + // Processes multiple packages by handling download and/or install. This also does the reporting and user interaction needed. // Required Args: None // Inputs: PackageSubContexts // Outputs: None - struct InstallMultiplePackages : public WorkflowTask + struct ProcessMultiplePackages : public WorkflowTask { - InstallMultiplePackages( + ProcessMultiplePackages( StringResource::StringId dependenciesReportMessage, HRESULT resultOnFailure, std::vector<HRESULT>&& ignorableInstallResults = {}, bool ensurePackageAgreements = true, bool ignoreDependencies = false, bool stopOnFailure = false, - bool refreshPathVariable = false) : - WorkflowTask("InstallMultiplePackages"), + bool refreshPathVariable = false): + WorkflowTask("ProcessMultiplePackages"), m_dependenciesReportMessage(dependenciesReportMessage), m_resultOnFailure(resultOnFailure), m_ignorableInstallResults(std::move(ignorableInstallResults)), diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -245,6 +245,79 @@ namespace AppInstaller::CLI::Workflow std::vector<Utility::Architecture> m_allowedArchitectures; }; + struct InstallerTypeComparator : public details::ComparisonField + { + InstallerTypeComparator(std::vector<InstallerTypeEnum> requirement) : + details::ComparisonField("Installer Type"), m_requirement(std::move(requirement)) + { + m_requirementAsString = Utility::ConvertContainerToString(m_requirement, InstallerTypeToString); + AICLI_LOG(CLI, Verbose, + << "InstallerType Comparator created with Required InstallerTypes: " << m_requirementAsString); + } + + static std::unique_ptr<InstallerTypeComparator> Create(const Execution::Args& args) + { + std::vector<InstallerTypeEnum> requirement; + + if (args.Contains(Execution::Args::Type::InstallerType)) + { + requirement.emplace_back(Manifest::ConvertToInstallerTypeEnum(std::string(args.GetArg(Execution::Args::Type::InstallerType)))); + } + + if (!requirement.empty()) + { + return std::make_unique<InstallerTypeComparator>(requirement); + } + else + { + return {}; + } + } + + InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override + { + if (!m_requirement.empty()) + { + for (auto requiredInstallerType : m_requirement) + { + // The installer is applicable if the installer type or nested installer type matches. (User should be allowed to specify 'zip') + if (installer.EffectiveInstallerType() == requiredInstallerType || installer.BaseInstallerType == requiredInstallerType) + { + return InapplicabilityFlags::None; + } + } + + return InapplicabilityFlags::InstallerType; + } + else + { + return InapplicabilityFlags::None; + } + } + + std::string ExplainInapplicable(const Manifest::ManifestInstaller& installer) override + { + std::string result = "InstallerType does not match required type: "; + result += InstallerTypeToString(installer.EffectiveInstallerType()); + result += "Required InstallerTypes: "; + result += m_requirementAsString; + return result; + } + + bool IsFirstBetter(const Manifest::ManifestInstaller& first, const Manifest::ManifestInstaller& second) override + { + // TODO: Current implementation assumes there is only a single installer type requirement. This needs to be updated + // once multiple installerType requirements and preferences are accepted. + UNREFERENCED_PARAMETER(first); + UNREFERENCED_PARAMETER(second); + return true; + } + + private: + std::vector<InstallerTypeEnum> m_requirement; + std::string m_requirementAsString; + }; + struct InstalledTypeComparator : public details::ComparisonField { InstalledTypeComparator(Manifest::InstallerTypeEnum installedType) : @@ -655,6 +728,7 @@ namespace AppInstaller::CLI::Workflow // Filter order is not important, but comparison order determines priority. // TODO: There are improvements to be made here around ordering, especially in the context of implicit vs explicit vs command line preferences. + AddComparator(InstallerTypeComparator::Create(context.Args)); AddComparator(InstalledTypeComparator::Create(installationMetadata)); AddComparator(LocaleComparator::Create(context.Args, installationMetadata)); AddComparator(ScopeComparator::Create(context)); diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -26,6 +26,7 @@ namespace AppInstaller::CLI::Workflow Scope = 0x20, MachineArchitecture = 0x40, Market = 0x80, + InstallerType = 0x100, }; DEFINE_ENUM_FLAG_OPERATORS(InapplicabilityFlags); diff --git a/src/AppInstallerCLICore/Workflows/PromptFlow.cpp b/src/AppInstallerCLICore/Workflows/PromptFlow.cpp @@ -369,12 +369,21 @@ namespace AppInstaller::CLI::Workflow }; // Gets all the prompts that may be displayed, in order of appearance - std::vector<std::unique_ptr<PackagePrompt>> GetPackagePrompts(bool ensureAgreementsAcceptance = true) + std::vector<std::unique_ptr<PackagePrompt>> GetPackagePrompts(bool ensureAgreementsAcceptance = true, bool installerDownloadOnly = false) { std::vector<std::unique_ptr<PackagePrompt>> result; - result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); - result.push_back(std::make_unique<InstallRootPrompt>()); - result.push_back(std::make_unique<InstallerAbortsTerminalPrompt>()); + + if (installerDownloadOnly) + { + result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); + } + else + { + result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); + result.push_back(std::make_unique<InstallRootPrompt>()); + result.push_back(std::make_unique<InstallerAbortsTerminalPrompt>()); + } + return result; } } @@ -407,7 +416,9 @@ namespace AppInstaller::CLI::Workflow void ShowPromptsForSinglePackage::operator()(Execution::Context& context) const { - for (auto& prompt : GetPackagePrompts()) + bool installerDownloadOnly = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly); + + for (auto& prompt : GetPackagePrompts(true, installerDownloadOnly)) { // Show the prompt if needed if (prompt->PackageNeedsPrompt(context)) diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -264,8 +264,8 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::PackageSubContexts>(std::move(packageSubContexts)); context.Reporter.Info() << std::endl; context << - InstallMultiplePackages( - Resource::String::InstallAndUpgradeCommandsReportDependencies, + ProcessMultiplePackages( + Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE, { APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE }); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -909,6 +909,7 @@ namespace AppInstaller::CLI::Workflow case OperationType::Install: case OperationType::Search: case OperationType::Show: + case OperationType::Download: default: context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; break; @@ -1143,7 +1144,6 @@ namespace AppInstaller::CLI::Workflow installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); } - ManifestComparator manifestComparator(context, installationMetadata); auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>()); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -41,6 +41,7 @@ namespace AppInstaller::CLI::Workflow Show, Uninstall, Upgrade, + Download, }; // A task in the workflow. diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -68,6 +68,8 @@ namespace AppInstallerCLIE2ETests // Installer filename public const string TestCommandExe = "testCommand.exe"; public const string AppInstallerTestExeInstallerExe = "AppInstallerTestExeInstaller.exe"; + public const string AppInstallerTestMsiInstallerMsi = "AppInstallerTestMsiInstaller.msi"; + public const string AppInstallerTestZipInstallerZip = "AppInstallerTestZipInstaller.zip"; // Test installers' package IDs public const string ExeInstallerPackageId = "AppInstallerTest.TestExeInstaller"; @@ -96,6 +98,7 @@ namespace AppInstallerCLIE2ETests // Locations public const string LocalAppData = "LocalAppData"; + public const string Dependencies = "Dependencies"; // Package dir public const string PortableExePackageDirName = $"{PortableExePackageId}_{TestSourceIdentifier}"; @@ -239,6 +242,8 @@ namespace AppInstallerCLIE2ETests public const int ERROR_MULTIPLE_UNINSTALL_FAILED = unchecked((int)0x8A150066); public const int ERROR_NOT_ALL_QUERIES_FOUND_SINGLE = unchecked((int)0x8A150067); public const int ERROR_PACKAGE_IS_PINNED = unchecked((int)0x8A150068); + public const int ERROR_PACKAGE_IS_STUB = unchecked((int)0x8A150069); + public const int ERROR_DOWNLOAD_DEPENDENCIES = unchecked((int)0x8A15006A); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIE2ETests/DownloadCommand.cs b/src/AppInstallerCLIE2ETests/DownloadCommand.cs @@ -0,0 +1,165 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DownloadCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using Microsoft.Management.Deployment; + using NUnit.Framework; + using Windows.System; + + /// <summary> + /// Test download command. + /// </summary> + public class DownloadCommand : BaseCommand + { + /// <summary> + /// One time setup. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("download", true); + WinGetSettingsHelper.ConfigureFeature("dependencies", true); + } + + /// <summary> + /// Downloads the test installer and its package dependencies. + /// </summary> + [Test] + public void DownloadDependencies() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.PackageDependency --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); + Assert.True(TestCommon.VerifyInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US")); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + } + + /// <summary> + /// Downloads the test installer and skips dependencies. + /// </summary> + [Test] + public void DownloadDependencies_Skip() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.PackageDependency --skip-dependencies --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Dependencies skipped.")); + Assert.IsFalse(Directory.Exists(Path.Combine(downloadDir, Constants.Dependencies))); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + } + + /// <summary> + /// Downloads the test installer to the default downloads directory. + /// </summary> + [Test] + public void DownloadToDefaultDirectory() + { + var packageVersion = "2.0.0.0"; + var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --version {packageVersion}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + } + + /// <summary> + /// Downloads the test installer to a specified directory. + /// </summary> + [Test] + public void DownloadToDirectory() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + } + + /// <summary> + /// Downloads the test installer using the user scope argument. + /// </summary> + [Test] + public void DownloadWithUserScope() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --scope user --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the machine scope argument. + /// </summary> + [Test] + public void DownloadWithMachineScope() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --scope machine --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the 'zip' installer type argument. Verifies that base installer types such as 'zip' are still supported. + /// </summary> + [Test] + public void DownloadWithZipInstallerTypeArg() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --installer-type zip --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + } + + /// <summary> + /// Downloads the test installer using the installer type argument. + /// </summary> + [Test] + public void DownloadWithInstallerTypeArg() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --installer-type msi --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the architecture argument. + /// </summary> + [Test] + public void DownloadWithArchitectureArg() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --architecture x86 --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the locale argument. + /// </summary> + [Test] + public void DownloadWithLocaleArg() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --locale zh-CN --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + } + + /// <summary> + /// Downloads the test installer with a hash mismatch. + /// </summary> + [Test] + public void DownloadWithHashMismatch() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var errorResult = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestExeSha256Mismatch --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALLER_HASH_MISMATCH, errorResult.ExitCode); + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -53,6 +53,8 @@ namespace AppInstallerCLIE2ETests WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); WinGetSettingsHelper.ConfigureFeature("directMSI", true); WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); + WinGetSettingsHelper.ConfigureFeature("download", true); + WinGetSettingsHelper.ConfigureFeature("dependencies", true); var result = TestCommon.RunAICLICommand("features", string.Empty); Assert.True(result.StdOut.Contains("Enabled")); } diff --git a/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs b/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs @@ -0,0 +1,320 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DownloadInterop.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.Interop +{ + using System; + using System.IO; + using System.Threading.Tasks; + using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment.Projection; + using NUnit.Framework; + using Windows.System; + + /// <summary> + /// Download interop. + /// </summary> + [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.InProcess), Category = nameof(InstanceInitializersSource.InProcess))] + [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))] + public class DownloadInterop : BaseInterop + { + private PackageManager packageManager; + private PackageCatalogReference testSource; + + /// <summary> + /// Initializes a new instance of the <see cref="DownloadInterop"/> class. + /// </summary> + /// <param name="initializer">Initializer.</param> + public DownloadInterop(IInstanceInitializer initializer) + : base(initializer) + { + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void SetUp() + { + this.packageManager = this.TestFactory.CreatePackageManager(); + this.testSource = this.packageManager.GetPackageCatalogByName(Constants.TestSourceName); + } + + /// <summary> + /// Downloads the test installer and its package dependencies. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadDependencies() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.PackageDependency"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); + Assert.True(TestCommon.VerifyInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US")); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + } + + /// <summary> + /// Downloads the test installer and skips dependencies. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadDependencies_Skip() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.PackageDependency"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.SkipDependencies = true; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); + Assert.IsFalse(Directory.Exists(dependenciesDir)); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + } + + /// <summary> + /// Download the installer to the default directory. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadToDefaultDirectory() + { + // Find package + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + var packageVersion = "2.0.0.0"; + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + } + + /// <summary> + /// Download the installer to a specified directory. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadToDirectory() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeInstaller"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + } + + /// <summary> + /// Download the installer using the user scope argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithUserScope() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.Scope = PackageInstallScope.User; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US")); + } + + /// <summary> + /// Download the installer using the machine scope argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithMachineScope() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.Scope = PackageInstallScope.System; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Download the test installer using the 'zip' installer type argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithZipInstallerTypeArg() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.InstallerType = PackageInstallerType.Zip; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + } + + /// <summary> + /// Downloads the test installer using the installer type argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithInstallerTypeArg() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.InstallerType = PackageInstallerType.Msi; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the architecture argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithArchitectureArg() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.Architecture = ProcessorArchitecture.X86; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + } + + /// <summary> + /// Downloads the test installer using the locale argument. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithLocaleArg() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestMultipleInstallers"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + downloadOptions.Locale = "zh-CN"; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + } + + /// <summary> + /// Downloads the test installer with a hash mismatch. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task DownloadWithHashMismatch() + { + // Find package + var downloadDir = TestCommon.GetRandomTestDir(); + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.TestExeSha256Mismatch"); + + // Configure installation + var downloadOptions = this.TestFactory.CreateDownloadOptions(); + downloadOptions.AcceptPackageAgreements = true; + downloadOptions.DownloadDirectory = downloadDir; + + // Download + var downloadResult = await this.packageManager.DownloadPackageAsync(searchResult.CatalogPackage, downloadOptions); + + // Assert + Assert.AreEqual(DownloadResultStatus.DownloadError, downloadResult.Status); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs b/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs @@ -21,7 +21,12 @@ namespace AppInstallerCLIE2ETests.Interop [OneTimeSetUp] public void Setup() { - TestCommon.SetupTestSource(); + TestCommon.SetupTestSource(); + + // For the COM E2E tests, once the settings file is initialized, it persists throughout the entirety of the tests. + // Any experimental features needed must be declared prior to running the COM E2E tests. + WinGetSettingsHelper.ConfigureFeature("dependencies", true); + WinGetSettingsHelper.ConfigureFeature("download", true); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -9,7 +9,6 @@ namespace AppInstallerCLIE2ETests using System; using System.IO; using Microsoft.Win32; - using Newtonsoft.Json; using NUnit.Framework; /// <summary> diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -11,8 +11,10 @@ namespace AppInstallerCLIE2ETests using System.IO; using System.Reflection; using System.Threading; + using Microsoft.Management.Deployment; using Microsoft.Win32; using NUnit.Framework; + using Windows.System; /// <summary> /// Test common. @@ -25,6 +27,11 @@ namespace AppInstallerCLIE2ETests public enum Scope { /// <summary> + /// None. + /// </summary> + Unknown, + + /// <summary> /// User. /// </summary> User, @@ -457,7 +464,7 @@ namespace AppInstallerCLIE2ETests } /// <summary> - /// Get portable symlink dir. + /// Gets the portable symlink directory. /// </summary> /// <param name="scope">Scope.</param> /// <returns>The path of the symlinks.</returns> @@ -474,7 +481,7 @@ namespace AppInstallerCLIE2ETests } /// <summary> - /// Get portable package directory. + /// Gets the portable package directory. /// </summary> /// <returns>The portable package directory.</returns> public static string GetPortablePackagesDirectory() @@ -483,6 +490,15 @@ namespace AppInstallerCLIE2ETests } /// <summary> + /// Gets the default download directory for the download command. + /// </summary> + /// <returns>The default download directory.</returns> + public static string GetDefaultDownloadDirectory() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); + } + + /// <summary> /// Verify portable package. /// </summary> /// <param name="installDir">Install dir.</param> @@ -596,6 +612,78 @@ namespace AppInstallerCLIE2ETests } /// <summary> + /// Verify installer downloaded correctly and cleanup. + /// </summary> + /// <param name="downloadDir">Download directory.</param> + /// <param name="name">Package name.</param> + /// <param name="version">Package version.</param> + /// <param name="arch">Installer architecture.</param> + /// <param name="scope">Installer scope.</param> + /// <param name="installerType">Installer type.</param> + /// <param name="locale">Installer locale.</param> + /// <param name="isArchive">Boolean value indicating whether the installer is an archive.</param> + /// <param name="cleanup">Boolean value indicating whether to remove the installer file and directory.</param> + /// <returns>True if success.</returns> + public static bool VerifyInstallerDownload( + string downloadDir, + string name, + string version, + Windows.System.ProcessorArchitecture arch, + Scope scope, + PackageInstallerType installerType, + string locale = null, + bool isArchive = false, + bool cleanup = true) + { + string expectedFileName = $"{name}_{version}"; + + if (scope != Scope.Unknown) + { + expectedFileName += $"_{scope}"; + } + + expectedFileName += $"_{arch}_{installerType}"; + + if (!string.IsNullOrEmpty(locale)) + { + expectedFileName += $"_{locale}"; + } + + string extension; + if (isArchive) + { + extension = ".zip"; + } + else + { + extension = installerType switch + { + PackageInstallerType.Msi => ".msi", + PackageInstallerType.Msix => ".msix", + _ => ".exe" + }; + } + + expectedFileName += extension; + string installerDownloadPath = Path.Combine(downloadDir, expectedFileName); + + bool downloadResult = false; + + if (Directory.Exists(downloadDir) && File.Exists(installerDownloadPath)) + { + downloadResult = true; + + if (cleanup) + { + File.Delete(installerDownloadPath); + Directory.Delete(downloadDir, true); + } + } + + return downloadResult; + } + + /// <summary> /// Verify exe installer correctly and then uninstall it. /// </summary> /// <param name="installDir">Install directory.</param> diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMultipleInstallers.yaml @@ -0,0 +1,38 @@ +PackageIdentifier: AppInstallerTest.TestMultipleInstallers +PackageVersion: 1.0.0.0 +PackageName: TestMultipleInstallers +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test manifest with multiple installers +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: nullsoft + InstallerSha256: <EXEHASH> + Scope: user + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi + InstallerSha256: <MSIHASH> + InstallerType: msi + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' + Scope: machine + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + InstallerLocale: zh-CN + ProductCode: '{E1880465-8CC2-4033-90AE-DE4E7FDBA26E}' + InstallerSha256: <ZIPHASH> + NestedInstallerType: exe + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestExeInstaller.exe + InstallerSwitches: + Custom: /execustom /productID {E1880465-8CC2-4033-90AE-DE4E7FDBA26E} + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestType: singleton +ManifestVersion: 1.4.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestHashHelper.cs b/src/AppInstallerCLIE2ETests/TestHashHelper.cs @@ -87,12 +87,14 @@ namespace AppInstallerCLIE2ETests text = text.Replace("<EXEHASH>", ExeInstallerHashValue); File.WriteAllText(file.FullName, text); } - else if (text.Contains("<MSIHASH>")) + + if (text.Contains("<MSIHASH>")) { text = text.Replace("<MSIHASH>", MsiInstallerHashValue); File.WriteAllText(file.FullName, text); } - else if (text.Contains("<MSIXHASH>")) + + if (text.Contains("<MSIXHASH>")) { text = text.Replace("<MSIXHASH>", MsixInstallerHashValue); @@ -103,7 +105,8 @@ namespace AppInstallerCLIE2ETests File.WriteAllText(file.FullName, text); } - else if (text.Contains("<ZIPHASH>")) + + if (text.Contains("<ZIPHASH>")) { text = text.Replace("<ZIPHASH>", ZipInstallerHashValue); File.WriteAllText(file.FullName, text); diff --git a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs @@ -43,6 +43,7 @@ namespace AppInstallerCLIE2ETests { "experimentalCmd", false }, { "dependencies", false }, { "directMSI", false }, + { "download", false }, } }, { @@ -193,6 +194,7 @@ namespace AppInstallerCLIE2ETests ConfigureFeature("pinning", status); ConfigureFeature("configuration", status); ConfigureFeature("windowsFeature", status); + ConfigureFeature("download", status); } } } diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -64,6 +64,8 @@ </com:Class> <com:Class Id ="C9ED7917-66AB-4E31-A92A-F65F18EF7933" DisplayName="Configuration Statics Server"> </com:Class> + <com:Class Id ="8EF324ED-367C-4880-83E5-BB2ABD0B72F6" DisplayName="DownloadOptions Server"> + </com:Class> </com:ExeServer> </com:ComServer> </com:Extension> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -980,9 +980,9 @@ Configuration is disabled due to Group Policy.</value> <value>The packages found in this import have the following dependencies:</value> <comment>Import command sentence showed before reporting dependencies</comment> </data> - <data name="InstallAndUpgradeCommandsReportDependencies" xml:space="preserve"> + <data name="PackageRequiresDependencies" xml:space="preserve"> <value>This package requires the following dependencies:</value> - <comment>Install and Upgrade commands sentence showed before reporting dependencies</comment> + <comment>Message shown before reporting dependencies</comment> </data> <data name="DependenciesFlowInstall" xml:space="preserve"> <value>Installing dependencies:</value> @@ -1322,8 +1322,8 @@ Please specify one of them using the --source option to proceed.</value> <value>Windows Package Manager (Preview) v{0}</value> <comment>{Locked="{0}"} Label displaying the preview product name and pre-release version. {0} is a placeholder replaced by the product version.</comment> </data> - <data name="InstallArchitectureArgumentDescription" xml:space="preserve"> - <value>Select the architecture to install</value> + <data name="ArchitectureArgumentDescription" xml:space="preserve"> + <value>Select the architecture</value> </data> <data name="IncludeUnknownArgumentDescription" xml:space="preserve"> <value>Upgrade packages even if their current version cannot be determined</value> @@ -1999,4 +1999,26 @@ Please specify one of them using the --source option to proceed.</value> <data name="ConfigurationWarningPromptTest" xml:space="preserve"> <value>Have you reviewed the configuration and would you like to proceed verifying it against the system?</value> </data> + <data name="DownloadCommandLongDescription" xml:space="preserve"> + <value>Downloads the installer from the selected package, either found by searching a configured source or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. By default, download command will download the appropriate installer to the user's Downloads folder.</value> + </data> + <data name="DownloadCommandShortDescription" xml:space="preserve"> + <value>Downloads the installer from a given package</value> + </data> + <data name="DownloadDirectoryArgumentDescription" xml:space="preserve"> + <value>Directory where the installers are downloaded to</value> + </data> + <data name="DependenciesFlowDownload" xml:space="preserve"> + <value>Downloading dependencies:</value> + </data> + <data name="InstallerDownloaded" xml:space="preserve"> + <value>Installer downloaded: {0}</value> + <comment>{Locked="{0}"} Full path of the downloaded installer.</comment> + </data> + <data name="InstallerTypeArgumentDescription" xml:space="preserve"> + <value>Select the installer type</value> + </data> + <data name="InstallerDownloads" xml:space="preserve"> + <value>Installer Downloads</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/InstallDependenciesFlow.cpp b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp @@ -41,10 +41,10 @@ void OverrideOpenDependencySource(TestContext& context) } }); } -void OverrideForInstallMultiplePackages(TestContext& context) +void OverrideForProcessMultiplePackages(TestContext& context) { - context.Override({ Workflow::InstallMultiplePackages( - Resource::String::InstallAndUpgradeCommandsReportDependencies, + context.Override({ Workflow::ProcessMultiplePackages( + Resource::String::PackageRequiresDependencies, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, @@ -64,7 +64,6 @@ TEST_CASE("DependencyGraph_SkipInstalled", "[InstallFlow][workflow][dependencyGr Manifest manifest = CreateFakeManifestWithDependencies("DependenciesInstalled"); OverrideOpenDependencySource(context); - OverrideForInstallMultiplePackages(context); context.Add<Execution::Data::DependencySource>(Source{ std::make_shared<DependenciesTestSource>() }); context.Add<Execution::Data::Manifest>(manifest); @@ -73,7 +72,7 @@ TEST_CASE("DependencyGraph_SkipInstalled", "[InstallFlow][workflow][dependencyGr TestUserSettings settings; settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); - context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + context << CreateDependencySubContexts(Resource::String::PackageRequiresDependencies); auto& dependencyPackages = context.Get<Execution::Data::PackageSubContexts>(); REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowContainsLoop)) == std::string::npos); @@ -89,7 +88,6 @@ TEST_CASE("DependencyGraph_validMinVersions", "[InstallFlow][workflow][dependenc auto previousThreadGlobals = context.SetForCurrentThread(); Manifest manifest = CreateFakeManifestWithDependencies("DependenciesValidMinVersions"); OverrideOpenDependencySource(context); - OverrideForInstallMultiplePackages(context); context.Add<Execution::Data::DependencySource>(Source{ std::make_shared<DependenciesTestSource>() }); context.Add<Execution::Data::Manifest>(manifest); @@ -98,7 +96,7 @@ TEST_CASE("DependencyGraph_validMinVersions", "[InstallFlow][workflow][dependenc TestUserSettings settings; settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); - context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + context << CreateDependencySubContexts(Resource::String::PackageRequiresDependencies); auto& dependencyPackages = context.Get<Execution::Data::PackageSubContexts>(); @@ -116,7 +114,6 @@ TEST_CASE("DependencyGraph_PathNoLoop", "[InstallFlow][workflow][dependencyGraph auto previousThreadGlobals = context.SetForCurrentThread(); Manifest manifest = CreateFakeManifestWithDependencies("PathBetweenBranchesButNoLoop"); OverrideOpenDependencySource(context); - OverrideForInstallMultiplePackages(context); context.Add<Execution::Data::DependencySource>(Source{ std::make_shared<DependenciesTestSource>() }); context.Add<Execution::Data::Manifest>(manifest); @@ -125,7 +122,7 @@ TEST_CASE("DependencyGraph_PathNoLoop", "[InstallFlow][workflow][dependencyGraph TestUserSettings settings; settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); - context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + context << CreateDependencySubContexts(Resource::String::PackageRequiresDependencies); auto& dependencyPackages = context.Get<Execution::Data::PackageSubContexts>(); @@ -213,7 +210,7 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies INFO(installOutput.str()); // Verify root dependencies are shown - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE(installOutput.str().find("PreviewIISOnRoot") != std::string::npos); } @@ -223,6 +220,7 @@ TEST_CASE("InstallerWithDependencies_SkipDependencies", "[dependencies]") TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); OverrideForShellExecute(context); + OverrideOpenDependencySource(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); context.Args.AddArg(Execution::Args::Type::SkipDependencies); @@ -235,7 +233,7 @@ TEST_CASE("InstallerWithDependencies_SkipDependencies", "[dependencies]") INFO(installOutput.str()); REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesSkippedMessage).get()) != std::string::npos); - REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE_FALSE(installOutput.str().find("PreviewIIS") != std::string::npos); } @@ -245,6 +243,7 @@ TEST_CASE("InstallerWithDependencies_IgnoreDependenciesSetting", "[dependencies] TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); OverrideForShellExecute(context); + OverrideOpenDependencySource(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); @@ -257,7 +256,7 @@ TEST_CASE("InstallerWithDependencies_IgnoreDependenciesSetting", "[dependencies] INFO(installOutput.str()); REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesSkippedMessage).get()) != std::string::npos); - REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE_FALSE(installOutput.str().find("PreviewIIS") != std::string::npos); } @@ -279,7 +278,7 @@ TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[depe INFO(installOutput.str()); // Verify installer dependencies are shown - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); // and root dependencies are not REQUIRE(installOutput.str().find("PreviewIISOnRoot") == std::string::npos); @@ -303,7 +302,7 @@ TEST_CASE("InstallFlow_Dependencies", "[InstallFlow][workflow][dependencies]") INFO(installOutput.str()); // Verify all types of dependencies are printed - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); } diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -635,7 +635,7 @@ TEST_CASE("UpdateFlow_Dependencies", "[UpdateFlow][workflow][dependencies]") std::string updateResultStr = updateOutput.str(); // Verify dependencies are informed - REQUIRE(updateResultStr.find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(updateResultStr.find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos); REQUIRE(updateResultStr.find("PreviewIIS") != std::string::npos); REQUIRE(updateResultStr.find("Preview VC Runtime") != std::string::npos); } diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp @@ -510,6 +510,21 @@ TEST_CASE("SettingsPortablePackageMachineRoot", "[settings]") } } +TEST_CASE("SettingsDownloadDefaultDirectory", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Valid path") + { + std::string_view json = R"({ "downloadBehavior": { "defaultDownloadDirectory": "C:/Foo/Bar" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get<Setting::DownloadDefaultDirectory>() == "C:/Foo/Bar"); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } +} + TEST_CASE("SettingsInstallScope", "[settings]") { auto again = DeleteUserSettingsFiles(); diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -46,6 +46,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFConfiguration>(); case ExperimentalFeature::Feature::WindowsFeature: return userSettings.Get<Setting::EFWindowsFeature>(); + case ExperimentalFeature::Feature::Download: + return userSettings.Get<Setting::EFDownload>(); default: THROW_HR(E_UNEXPECTED); } @@ -81,6 +83,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Configuration", "configuration", "https://aka.ms/winget-settings#configuration", Feature::Configuration }; case Feature::WindowsFeature: return ExperimentalFeature{ "Windows Feature Dependencies", "windowsFeature", "https://aka.ms/winget-settings", Feature::WindowsFeature }; + case Feature::Download: + return ExperimentalFeature{ "Download", "download", "https://aka.ms/winget-settings", Feature::Download }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -47,6 +47,8 @@ namespace AppInstaller::Runtime PortableLinksMachineLocation, // The root location for the package containing the winget application. SelfPackageRoot, + // The location where user downloads are stored. + UserProfileDownloads, // Always one more than the last path; for being able to iterate paths in tests. Max }; diff --git a/src/AppInstallerCommonCore/Public/winget/DependenciesGraph.h b/src/AppInstallerCommonCore/Public/winget/DependenciesGraph.h @@ -7,7 +7,7 @@ namespace AppInstaller::Manifest { struct DependencyGraph { - // this constructor was intented for use during installation flow (we already have installer dependencies and there's no need to search the source again) + // this constructor was intended for use during installation flow (we already have installer dependencies and there's no need to search the source again) DependencyGraph(const Dependency& root, const DependencyList& rootDependencies, std::function<const DependencyList(const Dependency&)> infoFunction); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -26,6 +26,7 @@ namespace AppInstaller::Settings DirectMSI = 0x2, Configuration = 0x4, WindowsFeature = 0x8, + Download = 0x10, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -73,6 +73,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFConfiguration, EFWindowsFeature, + EFDownload, // Telemetry TelemetryDisable, // Install behavior @@ -95,6 +96,8 @@ namespace AppInstaller::Settings LoggingLevelPreference, // Uninstall behavior UninstallPurgePortablePackage, + // Download behavior + DownloadDefaultDirectory, // Interactivity InteractivityDisable, #ifndef AICLI_DISABLE_TEST_HOOKS @@ -147,6 +150,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration, bool, bool, false, ".experimentalFeatures.configuration"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFWindowsFeature, bool, bool, false, ".experimentalFeatures.windowsFeature"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFDownload, bool, bool, false, ".experimentalFeatures.download"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior @@ -163,6 +167,9 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallDefaultRoot, std::string, std::filesystem::path, {}, ".installBehavior.defaultInstallRoot"sv); // Uninstall behavior SETTINGMAPPING_SPECIALIZATION(Setting::UninstallPurgePortablePackage, bool, bool, false, ".uninstallBehavior.purgePortablePackage"sv); + // Download behavior + SETTINGMAPPING_SPECIALIZATION(Setting::DownloadDefaultDirectory, std::string, std::filesystem::path, {}, ".downloadBehavior.defaultDownloadDirectory"sv); + // Network SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDownloader, std::string, InstallerDownloader, InstallerDownloader::Default, ".network.downloader"sv); SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDOProgressTimeoutInSeconds, uint32_t, std::chrono::seconds, 60s, ".network.doProgressTimeoutInSeconds"sv); diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -382,6 +382,10 @@ namespace AppInstaller::Runtime result.Path /= s_PortablePackageRoot; result.Path /= s_LinksDirectory; break; + case PathName::UserProfileDownloads: + result.Path = GetKnownFolderPath(FOLDERID_Downloads); + mayBeInProfilePath = true; + break; default: THROW_HR(E_UNEXPECTED); } @@ -448,10 +452,9 @@ namespace AppInstaller::Runtime case PathName::PortablePackageMachineRoot: case PathName::PortablePackageMachineRootX86: case PathName::PortableLinksMachineLocation: - result = GetPathDetailsCommon(path, forDisplay); - break; case PathName::PortableLinksUserLocation: case PathName::PortablePackageUserRoot: + case PathName::UserProfileDownloads: result = GetPathDetailsCommon(path, forDisplay); break; case PathName::SelfPackageRoot: @@ -526,10 +529,9 @@ namespace AppInstaller::Runtime case PathName::PortablePackageMachineRoot: case PathName::PortablePackageMachineRootX86: case PathName::PortableLinksMachineLocation: - result = GetPathDetailsCommon(path, forDisplay); - break; case PathName::PortableLinksUserLocation: case PathName::PortablePackageUserRoot: + case PathName::UserProfileDownloads: result = GetPathDetailsCommon(path, forDisplay); break; case PathName::SelfPackageRoot: diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -262,6 +262,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFConfiguration) WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) + WINGET_VALIDATE_PASS_THROUGH(EFDownload) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) @@ -349,6 +350,11 @@ namespace AppInstaller::Settings return ValidatePathValue(value); } + WINGET_VALIDATE_SIGNATURE(DownloadDefaultDirectory) + { + return ValidatePathValue(value); + } + WINGET_VALIDATE_SIGNATURE(NetworkDownloader) { static constexpr std::string_view s_downloader_default = "default"; diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -226,6 +226,8 @@ namespace AppInstaller return "The package currently installed is the stub package"; case APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED: return "Application shutdown signal received"; + case APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES: + return "Failed to download package dependencies."; // Install errors case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -119,6 +119,7 @@ #define APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED ((HRESULT)0x8A150068) #define APPINSTALLER_CLI_ERROR_PACKAGE_IS_STUB ((HRESULT)0x8A150069) #define APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED ((HRESULT)0x8A15006A) +#define APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES ((HRESULT)0x8A15006B) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest @@ -28,6 +28,10 @@ threadingModel="Both" description="UninstallOptions"/> <comClass + clsid="{4288DF96-FDC9-4B68-B403-193DBBF56A24}" + threadingModel="Both" + description="DownloadOptions"/> + <comClass clsid="{57DC8962-7343-42CD-B91C-04F6A25DB1D0}" threadingModel="Both" description="PackageMatchFilter"/> diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -68,6 +68,18 @@ namespace Microsoft.Management.Deployment.Projection [ClsidContext.OutOfProc] = new Guid("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"), [ClsidContext.OutOfProcDev] = new Guid("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"), } + }, + + [typeof(DownloadOptions)] = new() + { + ProjectedClassType = typeof(DownloadOptions), + InterfaceType = typeof(IDownloadOptions), + Clsids = new Dictionary<ClsidContext, Guid>() + { + [ClsidContext.InProc] = new Guid("4288DF96-FDC9-4B68-B403-193DBBF56A24"), + [ClsidContext.OutOfProc] = new Guid("4CBABE76-7322-4BE4-9CEA-2589A80682DC"), + [ClsidContext.OutOfProcDev] = new Guid("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"), + } }, [typeof(PackageMatchFilter)] = new() diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -27,6 +27,8 @@ namespace Microsoft.Management.Deployment.Projection public UninstallOptions CreateUninstallOptions() => InstanceInitializer.CreateInstance<UninstallOptions>(); + public DownloadOptions CreateDownloadOptions() => InstanceInitializer.CreateInstance<DownloadOptions>(); + public PackageMatchFilter CreatePackageMatchFilter() => InstanceInitializer.CreateInstance<PackageMatchFilter>(); public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance<PackageManagerSettings>(); diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -12,6 +12,7 @@ #include "UninstallOptions.h" #include "PackageMatchFilter.h" #include "PackageManagerSettings.h" +#include "DownloadOptions.h" #pragma warning( pop ) namespace winrt::Microsoft::Management::Deployment @@ -38,6 +39,10 @@ namespace winrt::Microsoft::Management::Deployment { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::UninstallOptions); } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_DownloadOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::DownloadOptions); + } else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageMatchFilter)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -293,6 +293,37 @@ namespace winrt::Microsoft::Management::Deployment::implementation return Microsoft::Management::Deployment::PackageInstallerType::Unknown; } + ::AppInstaller::Manifest::InstallerTypeEnum GetManifestInstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType installerType) + { + switch (installerType) + { + case Microsoft::Management::Deployment::PackageInstallerType::Burn: + return ::AppInstaller::Manifest::InstallerTypeEnum::Burn; + case Microsoft::Management::Deployment::PackageInstallerType::Exe: + return ::AppInstaller::Manifest::InstallerTypeEnum::Exe; + case Microsoft::Management::Deployment::PackageInstallerType::Inno: + return ::AppInstaller::Manifest::InstallerTypeEnum::Inno; + case Microsoft::Management::Deployment::PackageInstallerType::Msi: + return ::AppInstaller::Manifest::InstallerTypeEnum::Msi; + case Microsoft::Management::Deployment::PackageInstallerType::Msix: + return ::AppInstaller::Manifest::InstallerTypeEnum::Msix; + case Microsoft::Management::Deployment::PackageInstallerType::MSStore: + return ::AppInstaller::Manifest::InstallerTypeEnum::MSStore; + case Microsoft::Management::Deployment::PackageInstallerType::Nullsoft: + return ::AppInstaller::Manifest::InstallerTypeEnum::Nullsoft; + case Microsoft::Management::Deployment::PackageInstallerType::Portable: + return ::AppInstaller::Manifest::InstallerTypeEnum::Portable; + case Microsoft::Management::Deployment::PackageInstallerType::Wix: + return ::AppInstaller::Manifest::InstallerTypeEnum::Wix; + case Microsoft::Management::Deployment::PackageInstallerType::Zip: + return ::AppInstaller::Manifest::InstallerTypeEnum::Zip; + case Microsoft::Management::Deployment::PackageInstallerType::Unknown: + return ::AppInstaller::Manifest::InstallerTypeEnum::Unknown; + } + + return ::AppInstaller::Manifest::InstallerTypeEnum::Unknown; + } + winrt::Microsoft::Management::Deployment::PackageInstallerScope GetDeploymentInstallerScope(::AppInstaller::Manifest::ScopeEnum installerScope) { switch (installerScope) diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h @@ -18,6 +18,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::optional<::AppInstaller::Utility::Architecture> GetUtilityArchitecture(winrt::Windows::System::ProcessorArchitecture architecture); std::optional<winrt::Windows::System::ProcessorArchitecture> GetWindowsSystemProcessorArchitecture(::AppInstaller::Utility::Architecture architecture); std::pair<::AppInstaller::Manifest::ScopeEnum, bool> GetManifestScope(winrt::Microsoft::Management::Deployment::PackageInstallScope scope); + ::AppInstaller::Manifest::InstallerTypeEnum GetManifestInstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType installerType); winrt::Microsoft::Management::Deployment::PackageInstallerType GetDeploymentInstallerType(::AppInstaller::Manifest::InstallerTypeEnum installerType); winrt::Microsoft::Management::Deployment::PackageInstallerScope GetDeploymentInstallerScope(::AppInstaller::Manifest::ScopeEnum installerScope); ::AppInstaller::Manifest::ScopeEnum GetManifestUninstallScope(winrt::Microsoft::Management::Deployment::PackageUninstallScope scope); @@ -26,7 +27,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::IconResolution GetDeploymentIconResolution(::AppInstaller::Manifest::IconResolutionEnum iconResolution); winrt::Microsoft::Management::Deployment::IconTheme GetDeploymentIconTheme(::AppInstaller::Manifest::IconThemeEnum iconTheme); -#define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_) \ +#define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_) \ if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::InstallResultStatus>) \ { \ resultStatus = TStatus::_installResultStatus_; \ @@ -35,6 +36,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { \ resultStatus = TStatus::_uninstallResultStatus_; \ } \ + else if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::DownloadResultStatus>) \ + { \ + resultStatus = TStatus::_downloadResultStatus_; \ + } \ template <typename TStatus> TStatus GetOperationResultStatus(::AppInstaller::CLI::Workflow::ExecutionStage executionStage, winrt::hresult hresult) @@ -61,19 +66,19 @@ namespace winrt::Microsoft::Management::Deployment::implementation resultStatus = TStatus::InvalidOptions; break; case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER: - WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableInstallers, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableInstallers, InternalError, NoApplicableInstallers); break; case APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE: case APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_UNKNOWN: case APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_NOT_NEWER: - WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableUpgrade, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableUpgrade, InternalError, InternalError); break; case APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND: case APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED: - WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError); + WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError); break; case APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED: - WINGET_GET_OPERATION_RESULT_STATUS(PackageAgreementsNotAccepted, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(PackageAgreementsNotAccepted, InternalError, PackageAgreementsNotAccepted); break; case APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX: case APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED: @@ -101,13 +106,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation resultStatus = TStatus::CatalogError; break; case ::AppInstaller::CLI::Workflow::ExecutionStage::Download: - WINGET_GET_OPERATION_RESULT_STATUS(DownloadError, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(DownloadError, InternalError, DownloadError); break; case ::AppInstaller::CLI::Workflow::ExecutionStage::PreExecution: resultStatus = TStatus::InternalError; break; case ::AppInstaller::CLI::Workflow::ExecutionStage::Execution: - WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError); + WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError); break; case ::AppInstaller::CLI::Workflow::ExecutionStage::PostExecution: resultStatus = TStatus::InternalError; diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.cpp b/src/Microsoft.Management.Deployment/DownloadOptions.cpp @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include <wil\cppwinrt_wrl.h> +// 4467 Allow use of uuid attribute for com object creation. +#include "DownloadOptions.h" +#pragma warning( pop ) +#include "DownloadOptions.g.cpp" +#include "Helpers.h" + +#include <AppInstallerArchitecture.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + DownloadOptions::DownloadOptions() + { + } + winrt::Microsoft::Management::Deployment::PackageVersionId DownloadOptions::PackageVersionId() + { + return m_packageVersionId; + } + void DownloadOptions::PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value) + { + m_packageVersionId = value; + } + winrt::Microsoft::Management::Deployment::PackageInstallScope DownloadOptions::Scope() + { + return m_scope; + } + void DownloadOptions::Scope(winrt::Microsoft::Management::Deployment::PackageInstallScope const& value) + { + m_scope = value; + } + winrt::Microsoft::Management::Deployment::PackageInstallerType DownloadOptions::InstallerType() + { + return m_installerType; + } + void DownloadOptions::InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value) + { + m_installerType = value; + } + winrt::Windows::System::ProcessorArchitecture DownloadOptions::Architecture() + { + return m_architecture; + } + void DownloadOptions::Architecture(winrt::Windows::System::ProcessorArchitecture const& value) + { + m_architecture = value; + } + hstring DownloadOptions::Locale() + { + return hstring(m_locale); + } + void DownloadOptions::Locale(hstring const& value) + { + m_locale = value; + } + hstring DownloadOptions::DownloadDirectory() + { + return hstring(m_downloadDirectory); + } + void DownloadOptions::DownloadDirectory(hstring const& value) + { + m_downloadDirectory = value; + } + bool DownloadOptions::AllowHashMismatch() + { + return m_allowHashMismatch; + } + void DownloadOptions::AllowHashMismatch(bool value) + { + m_allowHashMismatch = value; + } + bool DownloadOptions::SkipDependencies() + { + return m_skipDependencies; + } + void DownloadOptions::SkipDependencies(bool value) + { + m_skipDependencies = value; + } + bool DownloadOptions::AcceptPackageAgreements() + { + return m_acceptPackageAgreements; + } + void DownloadOptions::AcceptPackageAgreements(bool value) + { + m_acceptPackageAgreements = value; + } + hstring DownloadOptions::CorrelationData() + { + return hstring(m_correlationData); + } + void DownloadOptions::CorrelationData(hstring const& value) + { + m_correlationData = value; + } + CoCreatableMicrosoftManagementDeploymentClass(DownloadOptions); +} diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.h b/src/Microsoft.Management.Deployment/DownloadOptions.h @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DownloadOptions.g.h" +#include "Public/ComClsids.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_DownloadOptions)] + struct DownloadOptions : DownloadOptionsT<DownloadOptions> + { + DownloadOptions(); + + winrt::Microsoft::Management::Deployment::PackageVersionId PackageVersionId(); + void PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value); + winrt::Microsoft::Management::Deployment::PackageInstallScope Scope(); + void Scope(winrt::Microsoft::Management::Deployment::PackageInstallScope const& value); + winrt::Microsoft::Management::Deployment::PackageInstallerType InstallerType(); + void InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value); + winrt::Windows::System::ProcessorArchitecture Architecture(); + void Architecture(winrt::Windows::System::ProcessorArchitecture const& value); + hstring Locale(); + void Locale(hstring const& value); + hstring DownloadDirectory(); + void DownloadDirectory(hstring const& value); + bool AllowHashMismatch(); + void AllowHashMismatch(bool value); + bool SkipDependencies(); + void SkipDependencies(bool value); + bool AcceptPackageAgreements(); + void AcceptPackageAgreements(bool value); + hstring CorrelationData(); + void CorrelationData(hstring const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PackageVersionId m_packageVersionId{ nullptr }; + winrt::Microsoft::Management::Deployment::PackageInstallScope m_scope = winrt::Microsoft::Management::Deployment::PackageInstallScope::Any; + winrt::Microsoft::Management::Deployment::PackageInstallerType m_installerType = winrt::Microsoft::Management::Deployment::PackageInstallerType::Unknown; + winrt::Windows::System::ProcessorArchitecture m_architecture = winrt::Windows::System::ProcessorArchitecture::Unknown; + std::wstring m_locale = L""; + std::wstring m_downloadDirectory = L""; + bool m_allowHashMismatch = false; + bool m_skipDependencies = false; + bool m_acceptPackageAgreements = true; + std::wstring m_correlationData = L""; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct DownloadOptions : DownloadOptionsT<DownloadOptions, implementation::DownloadOptions> + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/DownloadResult.cpp b/src/Microsoft.Management.Deployment/DownloadResult.cpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DownloadResult.h" +#include "DownloadResult.g.cpp" +#include <wil\cppwinrt_wrl.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void DownloadResult::Initialize( + winrt::Microsoft::Management::Deployment::DownloadResultStatus status, + winrt::hresult extendedErrorCode, + hstring const& correlationData) + { + m_status = status; + m_extendedErrorCode = extendedErrorCode; + m_correlationData = correlationData; + } + hstring DownloadResult::CorrelationData() + { + return hstring(m_correlationData); + } + winrt::Microsoft::Management::Deployment::DownloadResultStatus DownloadResult::Status() + { + return m_status; + } + winrt::hresult DownloadResult::ExtendedErrorCode() + { + return m_extendedErrorCode; + } +} diff --git a/src/Microsoft.Management.Deployment/DownloadResult.h b/src/Microsoft.Management.Deployment/DownloadResult.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DownloadResult.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct DownloadResult : DownloadResultT<DownloadResult> + { + DownloadResult() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + winrt::Microsoft::Management::Deployment::DownloadResultStatus status, + winrt::hresult extendedErrorCode, + hstring const& correlationData); +#endif + + hstring CorrelationData(); + winrt::Microsoft::Management::Deployment::DownloadResultStatus Status(); + winrt::hresult ExtendedErrorCode(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + std::wstring m_correlationData = L""; + winrt::Microsoft::Management::Deployment::DownloadResultStatus m_status = winrt::Microsoft::Management::Deployment::DownloadResultStatus::Ok; + winrt::hresult m_extendedErrorCode = S_OK; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -189,6 +189,8 @@ <ClInclude Include="Converters.h" /> <ClInclude Include="CreateCompositePackageCatalogOptions.h" /> <ClInclude Include="Documentation.h" /> + <ClInclude Include="DownloadOptions.h" /> + <ClInclude Include="DownloadResult.h" /> <ClInclude Include="FindPackagesOptions.h" /> <ClInclude Include="FindPackagesResult.h" /> <ClInclude Include="Helpers.h" /> @@ -224,6 +226,8 @@ <ClCompile Include="Converters.cpp" /> <ClCompile Include="CreateCompositePackageCatalogOptions.cpp" /> <ClCompile Include="Documentation.cpp" /> + <ClCompile Include="DownloadOptions.cpp" /> + <ClCompile Include="DownloadResult.cpp" /> <ClCompile Include="FindPackagesOptions.cpp" /> <ClCompile Include="FindPackagesResult.cpp" /> <ClCompile Include="Helpers.cpp" /> diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -2,14 +2,21 @@ <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <ClCompile Include="CatalogPackage.cpp" /> + <ClCompile Include="CheckInstalledStatusResult.cpp" /> + <ClCompile Include="ComClsids.cpp" /> <ClCompile Include="ConnectResult.cpp" /> <ClCompile Include="Converters.cpp" /> <ClCompile Include="CreateCompositePackageCatalogOptions.cpp" /> + <ClCompile Include="Documentation.cpp" /> + <ClCompile Include="DownloadOptions.cpp" /> + <ClCompile Include="DownloadResult.cpp" /> <ClCompile Include="FindPackagesOptions.cpp" /> <ClCompile Include="FindPackagesResult.cpp" /> <ClCompile Include="Helpers.cpp" /> + <ClCompile Include="Icon.cpp" /> <ClCompile Include="InstallOptions.cpp" /> <ClCompile Include="InstallResult.cpp" /> + <ClCompile Include="InstalledStatus.cpp" /> <ClCompile Include="MatchResult.cpp" /> <ClCompile Include="PackageAgreement.cpp" /> <ClCompile Include="PackageCatalog.cpp" /> @@ -25,49 +32,46 @@ <ClCompile Include="SourceAgreement.cpp" /> <ClCompile Include="UninstallOptions.cpp" /> <ClCompile Include="UninstallResult.cpp" /> - <ClCompile Include="ComClsids.cpp" /> <ClCompile Include="PackageManagerSettings.cpp" /> <ClCompile Include="PackageInstallerInfo.cpp" /> - <ClCompile Include="InstalledStatus.cpp" /> <ClCompile Include="PackageInstallerInstalledStatus.cpp" /> - <ClCompile Include="CheckInstalledStatusResult.cpp" /> - <ClCompile Include="Documentation.cpp" /> - <ClCompile Include="Icon.cpp" /> </ItemGroup> <ItemGroup> <ClInclude Include="CatalogPackage.h" /> + <ClInclude Include="CatalogPackageMetadata.h" /> + <ClInclude Include="CheckInstalledStatusResult.h" /> <ClInclude Include="ConnectResult.h" /> <ClInclude Include="Converters.h" /> <ClInclude Include="CreateCompositePackageCatalogOptions.h" /> <ClInclude Include="Documentation.h" /> + <ClInclude Include="DownloadOptions.h" /> + <ClInclude Include="DownloadResult.h" /> <ClInclude Include="FindPackagesOptions.h" /> <ClInclude Include="FindPackagesResult.h" /> <ClInclude Include="Helpers.h" /> + <ClInclude Include="Icon.h" /> <ClInclude Include="InstallOptions.h" /> <ClInclude Include="InstallResult.h" /> + <ClInclude Include="InstalledStatus.h" /> <ClInclude Include="MatchResult.h" /> <ClInclude Include="PackageAgreement.h" /> <ClInclude Include="PackageCatalog.h" /> <ClInclude Include="PackageCatalogInfo.h" /> <ClInclude Include="PackageCatalogReference.h" /> + <ClInclude Include="PackageInstallerInfo.h" /> + <ClInclude Include="PackageInstallerInstalledStatus.h" /> <ClInclude Include="PackageManager.h" /> + <ClInclude Include="PackageManagerSettings.h" /> <ClInclude Include="PackageMatchFilter.h" /> <ClInclude Include="PackageVersionId.h" /> <ClInclude Include="PackageVersionInfo.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="SourceAgreement.h" /> <ClInclude Include="UninstallOptions.h" /> <ClInclude Include="UninstallResult.h" /> <ClInclude Include="Public\ComClsids.h"> <Filter>Public</Filter> </ClInclude> - <ClInclude Include="PackageManagerSettings.h" /> - <ClInclude Include="PackageInstallerInfo.h" /> - <ClInclude Include="InstalledStatus.h" /> - <ClInclude Include="PackageInstallerInstalledStatus.h" /> - <ClInclude Include="CheckInstalledStatusResult.h" /> - <ClInclude Include="CatalogPackageMetadata.h" /> - <ClInclude Include="SourceAgreement.h" /> - <ClInclude Include="Icon.h" /> <ClInclude Include="Public\CoCreatableMicrosoftManagementDeploymentClass.h"> <Filter>Public</Filter> </ClInclude> diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -10,6 +10,7 @@ #include <winget/UserSettings.h> #include <winget/Manifest.h> #include "Commands/COMCommand.h" +#include <AppInstallerArchitecture.h> #include <AppInstallerTelemetry.h> #include <AppInstallerErrors.h> #pragma warning( push ) @@ -21,13 +22,13 @@ #pragma warning( pop ) #include "PackageManager.g.cpp" #include "CatalogPackage.h" +#include "DownloadResult.h" #include "InstallResult.h" #include "UninstallResult.h" #include "PackageCatalogInfo.h" #include "PackageCatalogReference.h" #include "PackageVersionInfo.h" #include "PackageVersionId.h" -#include "Workflows/WorkflowBase.h" #include "Converters.h" #include "Helpers.h" #include "ContextOrchestrator.h" @@ -181,6 +182,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation return *uninstallResult; } + winrt::Microsoft::Management::Deployment::DownloadResult GetDownloadResult(::Workflow::ExecutionStage executionStage, winrt::hresult terminationHR, winrt::hstring correlationData) + { + winrt::Microsoft::Management::Deployment::DownloadResultStatus downloadResultStatus = GetOperationResultStatus<DownloadResultStatus>(executionStage, terminationHR); + auto downloadResult = winrt::make_self<wil::details::module_count_wrapper<winrt::Microsoft::Management::Deployment::implementation::DownloadResult>>(); + downloadResult->Initialize(downloadResultStatus, terminationHR, correlationData); + return *downloadResult; + } + + template <typename TResult> TResult GetOperationResult(::Workflow::ExecutionStage executionStage, winrt::hresult terminationHR, uint32_t operationError, winrt::hstring correlationData, bool rebootRequired) { @@ -192,6 +202,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return GetUninstallResult(executionStage, terminationHR, operationError, correlationData, rebootRequired); } + else if constexpr (std::is_same_v<TResult, winrt::Microsoft::Management::Deployment::DownloadResult>) + { + return GetDownloadResult(executionStage, terminationHR, correlationData); + } } #define WINGET_GET_PROGRESS_STATE(_installState_, _uninstallState_) \ @@ -226,9 +240,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation // We already reported queued progress up front. break; case ::Workflow::ExecutionStage::Download: - if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::InstallProgress>) + if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::InstallProgress> || + std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::PackageDownloadProgress>) { - progressState = PackageInstallProgressState::Downloading; + progressState = TState::Downloading; if (reportType == ReportType::BeginProgress) { reportProgress = true; @@ -295,6 +310,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation TProgress progress{ progressState, operationProgress }; return progress; } + else if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::PackageDownloadProgress>) + { + TProgress progress{ progressState, downloadBytesDownloaded, downloadBytesRequired, downloadProgress }; + return progress; + } } else { @@ -302,7 +322,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } - Microsoft::Management::Deployment::PackageVersionInfo GetPackageVersionInfo(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::InstallOptions options) + template <typename TOptions> + Microsoft::Management::Deployment::PackageVersionInfo GetPackageVersionInfo(winrt::Microsoft::Management::Deployment::CatalogPackage package, TOptions options) { Microsoft::Management::Deployment::PackageVersionInfo packageVersionInfo{ nullptr }; @@ -446,6 +467,56 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + void PopulateContextFromDownloadOptions( + ::AppInstaller::CLI::Execution::Context* context, + winrt::Microsoft::Management::Deployment::DownloadOptions options) + { + if (options) + { + if (!options.DownloadDirectory().empty()) + { + context->Args.AddArg(Execution::Args::Type::DownloadDirectory, ::AppInstaller::Utility::ConvertToUTF8(options.DownloadDirectory())); + } + if (!options.Locale().empty()) + { + context->Args.AddArg(Execution::Args::Type::Locale, ::AppInstaller::Utility::ConvertToUTF8(options.Locale())); + } + if (options.AllowHashMismatch()) + { + context->Args.AddArg(Execution::Args::Type::HashOverride); + } + if (options.SkipDependencies()) + { + context->Args.AddArg(Execution::Args::Type::SkipDependencies); + } + if (options.AcceptPackageAgreements()) + { + context->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + } + auto manifestScope = GetManifestScope(options.Scope()); + if (manifestScope.first != ::AppInstaller::Manifest::ScopeEnum::Unknown) + { + context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(manifestScope.first)); + } + + auto architecture = options.Architecture(); + if (architecture != Windows::System::ProcessorArchitecture::Unknown) + { + auto convertedArchitecture = GetUtilityArchitecture(architecture); + if (convertedArchitecture) + { + context->Args.AddArg(Execution::Args::Type::InstallArchitecture, ToString(convertedArchitecture.value())); + } + } + + auto installerType = GetManifestInstallerType(options.InstallerType()); + if (installerType != AppInstaller::Manifest::InstallerTypeEnum::Unknown) + { + context->Args.AddArg(Execution::Args::Type::InstallerType, AppInstaller::Manifest::InstallerTypeToString(installerType)); + } + } + } + template <typename TOptions> std::unique_ptr<COMContext> CreateContextFromOperationOptions( TOptions options, @@ -465,6 +536,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { PopulateContextFromUninstallOptions(context.get(), options); } + else if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::DownloadOptions>) + { + PopulateContextFromDownloadOptions(context.get(), options); + } return context; } @@ -576,6 +651,21 @@ namespace winrt::Microsoft::Management::Deployment::implementation return Execution::OrchestratorQueueItemFactory::CreateItemForUninstall(std::wstring{ package.Id() }, std::wstring{ package.InstalledVersion().PackageCatalog().Info().Id() }, std::move(comContext)); } + std::unique_ptr<Execution::OrchestratorQueueItem> CreateQueueItemForDownload( + std::unique_ptr<::AppInstaller::CLI::Execution::COMContext> comContext, + winrt::Microsoft::Management::Deployment::CatalogPackage package, + winrt::Microsoft::Management::Deployment::DownloadOptions options) + { + // Add manifest and PackageVersion to context for download. + // If the version of the package is specified use that, otherwise use the default. + Microsoft::Management::Deployment::PackageVersionInfo packageVersionInfo = GetPackageVersionInfo(package, options); + AddPackageManifestToContext(packageVersionInfo, comContext.get()); + + comContext->SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly); + + return Execution::OrchestratorQueueItemFactory::CreateItemForDownload(std::wstring{ package.Id() }, std::wstring{ packageVersionInfo.PackageCatalog().Info().Id() }, std::move(comContext)); + } + template <typename TResult, typename TProgress, typename TOptions, typename TProgressState> winrt::Windows::Foundation::IAsyncOperationWithProgress<TResult, TProgress> GetPackageOperation( bool canCancelQueueItem, @@ -612,6 +702,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { queueItem = CreateQueueItemForUninstall(std::move(comContext), package); } + else if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::DownloadOptions>) + { + queueItem = CreateQueueItemForDownload(std::move(comContext), package, options); + } Execution::ContextOrchestrator::Instance().EnqueueAndRunItem(queueItem); @@ -625,6 +719,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation TProgress queuedProgress{ TProgressState::Queued, 0}; report_progress(queuedProgress); } + else if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::PackageDownloadProgressState>) + { + TProgress queuedProgress{ TProgressState::Queued, 0 }; + report_progress(queuedProgress); + } } { // correlation data is not passed in when retrieving an existing queue item, so get it from the existing context. @@ -883,5 +982,70 @@ namespace winrt::Microsoft::Management::Deployment::implementation canCancelQueueItem, std::move(queueItem)); } +#define WINGET_RETURN_DOWNLOAD_RESULT_HR_IF(hr, boolVal) { if(boolVal) { return GetEmptyAsynchronousResultForOperation<Deployment::DownloadResult, Deployment::PackageDownloadProgress>(hr, correlationData); }} +#define WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(hr) { WINGET_RETURN_DOWNLOAD_RESULT_HR_IF(hr, FAILED(hr)) } + + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::DownloadResult, winrt::Microsoft::Management::Deployment::PackageDownloadProgress> PackageManager::DownloadPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::DownloadOptions options) + { + // TODO: Remove once 'download' experimental feature is stable. Dependencies experimental feature is also required to handle multiple package downloads. + if (!AppInstaller::Settings::ExperimentalFeature::IsEnabled(AppInstaller::Settings::ExperimentalFeature::Feature::Download)) + { + THROW_HR(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + + hstring correlationData = (options) ? options.CorrelationData() : L""; + + // options and catalog can both be null, package must be set. + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); + + HRESULT hr = S_OK; + std::wstring callerProcessInfoString; + try + { + // Check for permissions and get caller info for telemetry. + // This must be done before any co_awaits since it requires info from the rpc caller thread. + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(hrGetCallerId); + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + callerProcessInfoString = TryGetCallerProcessInfo(callerProcessId); + } + WINGET_CATCH_STORE(hr, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(hr); + + return GetPackageOperation<Deployment::DownloadResult, Deployment::PackageDownloadProgress, Deployment::DownloadOptions, Deployment::PackageDownloadProgressState>( + true /*canCancelQueueItem*/, nullptr /*queueItem*/, package, options, std::move(callerProcessInfoString)); + } + + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::DownloadResult, winrt::Microsoft::Management::Deployment::PackageDownloadProgress> PackageManager::GetDownloadProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo) + { + // TODO: Remove once 'download' experimental feature is stable. + if (!AppInstaller::Settings::ExperimentalFeature::IsEnabled(AppInstaller::Settings::ExperimentalFeature::Feature::Download)) + { + THROW_HR(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + + hstring correlationData; + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); + + HRESULT hr = S_OK; + std::shared_ptr<Execution::OrchestratorQueueItem> queueItem = nullptr; + try + { + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + + // Get the queueItem synchronously. + queueItem = GetExistingQueueItemForPackage(package, catalogInfo); + if (queueItem == nullptr || + queueItem->GetPackageOperationType() != PackageOperationType::Download) + { + return nullptr; + } + } + WINGET_CATCH_STORE(hr, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); + WINGET_RETURN_DOWNLOAD_RESULT_HR_IF_FAILED(hr); + + return GetPackageOperation<Deployment::DownloadResult, Deployment::PackageDownloadProgress, Deployment::DownloadOptions, Deployment::PackageDownloadProgressState>(true, std::move(queueItem)); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h @@ -36,6 +36,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation UninstallPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::UninstallOptions options); winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::UninstallResult, winrt::Microsoft::Management::Deployment::UninstallProgress> GetUninstallProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo); + // Contract 7.0 + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::DownloadResult, winrt::Microsoft::Management::Deployment::PackageDownloadProgress> + DownloadPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::DownloadOptions options); + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::DownloadResult, winrt::Microsoft::Management::Deployment::PackageDownloadProgress> + GetDownloadProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -151,6 +151,69 @@ namespace Microsoft.Management.Deployment UInt32 UninstallerErrorCode { get; }; } + /// State of the download + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + enum PackageDownloadProgressState + { + /// The download is queued but not yet active. Cancellation of the IAsyncOperationWithProgress in this + /// state will prevent the package from downloading. + Queued, + /// The installer is downloading. Cancellation of the IAsyncOperationWithProgress in this state will + /// end the download. + Downloading, + /// The operation has completed. + Finished, + }; + + /// Status of the download call + /// Implementation Note: Errors mapped from AppInstallerErrors.h + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + enum DownloadResultStatus + { + Ok, + BlockedByPolicy, + CatalogError, + InternalError, + InvalidOptions, + DownloadError, + ManifestError, + NoApplicableInstallers, + PackageAgreementsNotAccepted, + }; + + /// Result of the download + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + runtimeclass DownloadResult + { + /// Used by a caller to correlate the download with a caller's data. + String CorrelationData { get; }; + + /// Batched error code. + DownloadResultStatus Status { get; }; + + /// The error code of the overall operation. + HRESULT ExtendedErrorCode { get; }; + }; + + /// Progress object for the uninstall + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + struct PackageDownloadProgress + { + /// State of the download + PackageDownloadProgressState State; + + /// DESIGN NOTE: BytesDownloaded may only be available for downloads done by Windows Package Manager itself. + /// Number of bytes downloaded if known + UInt64 BytesDownloaded; + + /// DESIGN NOTE: BytesRequired may only be available for downloads done by Windows Package Manager itself. + /// Number of bytes required if known + UInt64 BytesRequired; + + /// Download percentage completed + Double DownloadProgress; + }; + /// IMPLEMENTATION NOTE: SourceOrigin from winget/RepositorySource.h /// Defines the origin of the package catalog details. [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 1)] @@ -882,6 +945,46 @@ namespace Microsoft.Management.Deployment } } + /// Options when downloading a package. + /// Intended to allow full compatibility with the "winget download" command line interface. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + runtimeclass DownloadOptions + { + DownloadOptions(); + + /// Optionally specifies the version from the package to download. If unspecified the version matching + /// CatalogPackage.GetLatestVersion() is used. + PackageVersionId PackageVersionId; + + /// The package installer type. + PackageInstallerType InstallerType; + + /// The package installer scope. + PackageInstallScope Scope; + + /// The package installer architecture. + Windows.System.ProcessorArchitecture Architecture; + + /// The package installer locale. + String Locale; + + /// The directory where the installers are downloaded to. + String DownloadDirectory; + + /// Continues the download even if the hash in the catalog does not match the linked installer. + Boolean AllowHashMismatch; + + /// Skip downloading the dependencies for the package. + Boolean SkipDependencies; + + /// Accept the package agreements required for download. + Boolean AcceptPackageAgreements; + + /// Used by a caller to correlate the download with a caller's data. + /// The string must be JSON encoded. + String CorrelationData; + } + /// IMPLEMENTATION NOTE: Documentation from AppInstaller::Manifest::Documentation [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 6)] runtimeclass Documentation @@ -1058,6 +1161,15 @@ namespace Microsoft.Management.Deployment /// Get uninstall progress Windows.Foundation.IAsyncOperationWithProgress<UninstallResult, UninstallProgress> GetUninstallProgress(CatalogPackage package, PackageCatalogInfo catalogInfo); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + { + // Download the specified package + Windows.Foundation.IAsyncOperationWithProgress<DownloadResult, PackageDownloadProgress> DownloadPackageAsync(CatalogPackage package, DownloadOptions options); + + // Get download progress + Windows.Foundation.IAsyncOperationWithProgress<DownloadResult, PackageDownloadProgress> GetDownloadProgress(CatalogPackage package, PackageCatalogInfo catalogInfo); + } } /// Global settings for PackageManager operations. @@ -1111,6 +1223,10 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView<UninstallOptions>; interface Windows.Foundation.Collections.IVector<UninstallResult>; interface Windows.Foundation.Collections.IVectorView<UninstallResult>; + interface Windows.Foundation.Collections.IVector<DownloadOptions>; + interface Windows.Foundation.Collections.IVectorView<DownloadOptions>; + interface Windows.Foundation.Collections.IVector<DownloadResult>; + interface Windows.Foundation.Collections.IVectorView<DownloadResult>; interface Windows.Foundation.Collections.IVector<MatchResult>; interface Windows.Foundation.Collections.IVectorView<MatchResult>; interface Windows.Foundation.Collections.IVector<PackageMatchFilter>; diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -12,6 +12,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_UninstallOptions "E1D9A11E-9F85-4D87-9C17-2B93143ADB8D" #define WINGET_OUTOFPROC_COM_CLSID_PackageMatchFilter "D02C9DAF-99DC-429C-B503-4E504E4AB000" #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "73D763B7-2937-432F-A97A-D98A4A596126" +#define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "4CBABE76-7322-4BE4-9CEA-2589A80682DC" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -20,6 +21,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_UninstallOptions "AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C" #define WINGET_OUTOFPROC_COM_CLSID_PackageMatchFilter "3F85B9F4-487A-4C48-9035-2903F8A6D9E8" #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "C9ED7917-66AB-4E31-A92A-F65F18EF7933" +#define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "8EF324ED-367C-4880-83E5-BB2ABD0B72F6" #endif // Clsids only used in in-proc invocation @@ -35,6 +37,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_UninstallOptions = { 0x869CB959, 0xEB54, 0x425C, 0xA1, 0xE4, 0x1A, 0x1C, 0x29, 0x1C, 0x64, 0xE9 }; // 869CB959-EB54-425C-A1E4-1A1C291C64E9 const CLSID WINGET_INPROC_COM_CLSID_PackageMatchFilter = { 0x57DC8962, 0x7343, 0x42CD, 0xB9, 0x1C, 0x04, 0xF6, 0xA2, 0x5D, 0xB1, 0xD0 }; // 57DC8962-7343-42CD-B91C-04F6A25DB1D0 const CLSID WINGET_INPROC_COM_CLSID_PackageManagerSettings = { 0x80CF9D63, 0x5505, 0x4342, 0xB9, 0xB4, 0xBB, 0x87, 0x89, 0x5C, 0xA8, 0xBB }; // 80CF9D63-5505-4342-B9B4-BB87895CA8BB + const CLSID WINGET_INPROC_COM_CLSID_DownloadOptions = { 0x4288DF96, 0xFDC9, 0x4B68, 0xB4, 0x03, 0x19, 0x3D, 0xBB, 0xF5, 0x6A, 0x24 }; // 4288DF96-FDC9-4B68-B403-193DBBF56A24 CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); } \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ComObjectFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ComObjectFactory.cs @@ -30,6 +30,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid InstallOptionsClsid = Guid.Parse("1095F097-EB96-453B-B4E6-1613637F3B14"); private static readonly Guid UninstallOptionsClsid = Guid.Parse("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"); private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); + private static readonly Guid DownloadOptionsClsid = Guid.Parse("4CBABE76-7322-4BE4-9CEA-2589A80682DC"); #else private static readonly Guid PackageManagerClsid = Guid.Parse("74CB3139-B7C5-4B9E-9388-E6616DEA288C"); private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"); @@ -37,6 +38,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid InstallOptionsClsid = Guid.Parse("44FE0580-62F7-44D4-9E91-AA9614AB3E86"); private static readonly Guid UninstallOptionsClsid = Guid.Parse("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"); private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); + private static readonly Guid DownloadOptionsClsid = Guid.Parse("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); @@ -50,6 +52,8 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Type UninstallOptionsType = Type.GetTypeFromCLSID(UninstallOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] + private static readonly Type DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); private static readonly Guid PackageManagerIid = Guid.Parse("B375E3B9-F2E0-5C93-87A7-B67497F7E593"); private static readonly Guid FindPackagesOptionsIid = Guid.Parse("A5270EDD-7DA7-57A3-BACE-F2593553561F"); @@ -57,6 +61,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid InstallOptionsIid = Guid.Parse("6EE9DB69-AB48-5E72-A474-33A924CD23B3"); private static readonly Guid UninstallOptionsIid = Guid.Parse("3EBC67F0-8339-594B-8A42-F90B69D02BBE"); private static readonly Guid PackageMatchFilterIid = Guid.Parse("D981ECA3-4DE5-5AD7-967A-698C7D60FC3B"); + private static readonly Guid DownloadOptionsIid = Guid.Parse("B4D72A63-40FF-597D-A7DA-43580268DC96"); /// <summary> /// Initializes static members of the <see cref="ComObjectFactory"/> class. @@ -116,6 +121,15 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } /// <summary> + /// Creates an instance of the <see cref="DownloadOptions" /> class. + /// </summary> + /// <returns>A <see cref="DownloadOptions" /> instance.</returns> + public virtual DownloadOptions CreateDownloadOptions() + { + return Create<DownloadOptions>(DownloadOptionsType, DownloadOptionsIid); + } + + /// <summary> /// Creates an instance of the <see cref="PackageMatchFilter" /> class. /// </summary> /// <returns>A <see cref="PackageMatchFilter" /> instance.</returns> diff --git a/src/WindowsPackageManager/main.cpp b/src/WindowsPackageManager/main.cpp @@ -23,6 +23,7 @@ CoCreatableClassWrlCreatorMapInclude(FindPackagesOptions); CoCreatableClassWrlCreatorMapInclude(CreateCompositePackageCatalogOptions); CoCreatableClassWrlCreatorMapInclude(InstallOptions); CoCreatableClassWrlCreatorMapInclude(UninstallOptions); +CoCreatableClassWrlCreatorMapInclude(DownloadOptions); CoCreatableClassWrlCreatorMapInclude(PackageMatchFilter); CoCreatableClassWrlCreatorMapInclude(PackageManagerSettings);