winget-cli

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

commit 7ff14f505add763cd59b906240761d07ca06c8e1
parent db1605c107d17e32763f56d92cfc3286354817d7
Author: KEINOS <github+fork-qiita-news@keinos.com>
Date:   Wed, 27 Aug 2025 16:33:44 +0000

Merge remote-tracking branch 'upstream/master'

Diffstat:
Msrc/AppInstallerCLI.sln | 9+++++++++
Msrc/AppInstallerCLICore/Argument.cpp | 2++
Msrc/AppInstallerCLICore/Commands/DownloadCommand.cpp | 189++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 1+
Msrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp | 6++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 3+++
Msrc/AppInstallerCLITests/MSStoreDownloadFlow.cpp | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/MSStoreDownload.cpp | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/AppInstallerCommonCore/Public/winget/MSStoreDownload.h | 3+++
Msrc/Microsoft.Management.Deployment/Converters.cpp | 13+++++++++++++
Msrc/Microsoft.Management.Deployment/Converters.h | 1+
Msrc/Microsoft.Management.Deployment/DownloadOptions.cpp | 33++++++++++++++++++++++++++++++++-
Msrc/Microsoft.Management.Deployment/DownloadOptions.h | 9+++++++++
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 17+++++++++++++++++
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 30++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ExportPackageCmdlet.cs | 30++++++++++++++++++++++++++++--
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSWindowsPlatform.cs | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/DownloadCommand.cs | 21+++++++++++++++++++--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs | 19+++++++++++++++++++
Msrc/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 | 2+-
21 files changed, 489 insertions(+), 127 deletions(-)

diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -5,8 +5,10 @@ VisualStudioVersion = 17.2.32630.192 MinimumVisualStudioVersion = 10.0.40219.1 Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "AppInstallerCLIPackage", "AppInstallerCLIPackage\AppInstallerCLIPackage.wapproj", "{6AA3791A-0713-4548-A357-87A323E7AC3A}" ProjectSection(ProjectDependencies) = postProject + {0BA531C8-CF0C-405B-8221-0FE51BA529D1} = {0BA531C8-CF0C-405B-8221-0FE51BA529D1} {1CC41A9A-AE66-459D-9210-1E572DD7BE69} = {1CC41A9A-AE66-459D-9210-1E572DD7BE69} {2B00D362-AC92-41F3-A8D2-5B1599BDCA01} = {2B00D362-AC92-41F3-A8D2-5B1599BDCA01} + {33745E4A-39E2-676F-7E23-50FB43848D25} = {33745E4A-39E2-676F-7E23-50FB43848D25} {5B6F90DF-FD19-4BAE-83D9-24DAD128E777} = {5B6F90DF-FD19-4BAE-83D9-24DAD128E777} {6597EB04-D105-49A7-A5A3-D27FE1DF895E} = {6597EB04-D105-49A7-A5A3-D27FE1DF895E} {CA460806-5E41-4E97-9A3D-1D74B433B663} = {CA460806-5E41-4E97-9A3D-1D74B433B663} @@ -215,6 +217,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetMCPServer", "WinGetMCPServer\WinGetMCPServer.csproj", "{33745E4A-39E2-676F-7E23-50FB43848D25}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F49C4C89-447E-4D15-B38B-5A8DCFB134AF}" + ProjectSection(SolutionItems) = preProject + PowerShell\scripts\Execute-WinGetTests.ps1 = PowerShell\scripts\Execute-WinGetTests.ps1 + PowerShell\scripts\Initialize-LocalWinGetModules.ps1 = PowerShell\scripts\Initialize-LocalWinGetModules.ps1 + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1046,6 +1054,7 @@ Global {A0B4F808-B190-41C4-97CB-C8EA1932F84F} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {F49C4C89-447E-4D15-B38B-5A8DCFB134AF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -265,6 +265,8 @@ namespace AppInstaller::CLI return { type, "platform"_liv, ArgTypeCategory::None }; case Execution::Args::Type::SkipMicrosoftStorePackageLicense: return { type, "skip-microsoft-store-package-license"_liv, "skip-license"_liv, ArgTypeCategory::None }; + case Execution::Args::Type::OSVersion: + return { type, "os-version"_liv, ArgTypeCategory::None }; // Common arguments case Execution::Args::Type::NoVT: diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp @@ -1,58 +1,59 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" #include "DownloadCommand.h" -#include "Workflows/CompletionFlow.h" -#include "Workflows/DownloadFlow.h" -#include "Workflows/InstallFlow.h" -#include "Workflows/PromptFlow.h" -#include "Resources.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/DownloadFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/PromptFlow.h" +#include "Resources.h" #include <AppInstallerRuntime.h> -#include <winget/ManifestCommon.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::InstallerArchitecture), - 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(Args::Type::CustomHeader), - Argument::ForType(Args::Type::AuthenticationMode), - Argument::ForType(Args::Type::AuthenticationAccount), - Argument::ForType(Args::Type::AcceptPackageAgreements), +#include <winget/ManifestCommon.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::InstallerArchitecture), + 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(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), + Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::SkipMicrosoftStorePackageLicense), - Argument::ForType(Args::Type::Platform), - }; - } - - Resource::LocString DownloadCommand::ShortDescription() const - { - return { Resource::String::DownloadCommandShortDescription }; - } - - Resource::LocString DownloadCommand::LongDescription() const - { - return { Resource::String::DownloadCommandLongDescription }; + Argument::ForType(Args::Type::Platform), + Argument{ Args::Type::OSVersion, Resource::String::OSVersionDescription, ArgumentType::Standard, Argument::Visibility::Help }, + }; + } + + Resource::LocString DownloadCommand::ShortDescription() const + { + return { Resource::String::DownloadCommandShortDescription }; + } + + Resource::LocString DownloadCommand::LongDescription() const + { + return { Resource::String::DownloadCommandLongDescription }; } void DownloadCommand::Complete(Context& context, Args::Type valueType) const @@ -82,15 +83,15 @@ namespace AppInstaller::CLI // Intentionally output nothing to allow pass through to filesystem. break; } - } - - Utility::LocIndView DownloadCommand::HelpLink() const - { - return "https://aka.ms/winget-command-download"_liv; - } - - void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const - { + } + + Utility::LocIndView DownloadCommand::HelpLink() const + { + return "https://aka.ms/winget-command-download"_liv; + } + + void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const + { Argument::ValidateCommonArguments(execArgs); if (execArgs.Contains(Execution::Args::Type::Platform)) @@ -103,39 +104,39 @@ namespace AppInstaller::CLI }); throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::Platform).Name(), validOptions)); } - } - } - - void DownloadCommand::ExecuteInternal(Context& context) const - { + } + } + + void DownloadCommand::ExecuteInternal(Context& context) const + { context.SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly); - context << Workflow::InitializeInstallerDownloadAuthenticatorsMap; - - 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; - } -} + context << Workflow::InitializeInstallerDownloadAuthenticatorsMap; + + 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/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -91,6 +91,7 @@ namespace AppInstaller::CLI::Execution DownloadDirectory, SkipMicrosoftStorePackageLicense, Platform, + OSVersion, // Setting Command AdminSettingEnable, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -491,6 +491,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined); WINGET_DEFINE_RESOURCE_STRINGID(Options); + WINGET_DEFINE_RESOURCE_STRINGID(OSVersionDescription); WINGET_DEFINE_RESOURCE_STRINGID(OutputDirectoryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -293,6 +293,12 @@ namespace AppInstaller::CLI::Workflow MSStoreDownloadContext downloadContext{ installer.ProductId, requiredArchitecture, requiredPlatform, requiredLocale, GetAuthenticationArguments(context) }; + if (context.Args.Contains(Execution::Args::Type::OSVersion)) + { + Utility::UInt64Version targetOSVersion{ std::string{ context.Args.GetArg(Execution::Args::Type::OSVersion) } }; + downloadContext.TargetOSVersion(std::move(targetOSVersion)); + } + MSStoreDownloadInfo downloadInfo; try { diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3387,4 +3387,7 @@ An unlocalized JSON fragment will follow on another line.</comment> <data name="PolicyEnableMcpServer" xml:space="preserve"> <value>Enable Windows Package Manager MCP Server</value> </data> + <data name="OSVersionDescription" xml:space="preserve"> + <value>Target OS version</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp b/src/AppInstallerCLITests/MSStoreDownloadFlow.cpp @@ -133,6 +133,19 @@ std::vector<SFS::AppContent> GetSfsAppContentsOverrideFunction(std::string_view dependencyX64); dependencyPackages.emplace_back(std::move(*dependencyX64)); + // Lower target OS dependency + std::unique_ptr<SFS::AppFile> dependencyX64_lower; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/dependency/x64", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Universal=9.0.0.0" }, + wuCategoryIdStr + ".Dependency_0.9.3.4_x64__8wekyb3d8bbwe", + dependencyX64_lower); + dependencyPackages.emplace_back(std::move(*dependencyX64_lower)); + std::unique_ptr<SFS::AppFile> dependencyArm; std::ignore = SFS::AppFile::Make( wuCategoryIdStr + ".appx", @@ -166,6 +179,19 @@ std::vector<SFS::AppContent> GetSfsAppContentsOverrideFunction(std::string_view packageX64); packages.emplace_back(std::move(*packageX64)); + // Good candidate x64, lower minimum OS version, lower package version + std::unique_ptr<SFS::AppFile> packageX64_lower; + std::ignore = SFS::AppFile::Make( + wuCategoryIdStr + ".appx", + "https://NotUsed/" + wuCategoryIdStr + "/x64", + 100, + { { SFS::HashType::Sha256, base64EncodedSha256 } }, + { SFS::Architecture::Amd64 }, + { "Desktop=9.0.0.0" }, + wuCategoryIdStr + "_0.9.0.0_x64__8wekyb3d8bbwe", + packageX64_lower); + packages.emplace_back(std::move(*packageX64_lower)); + // Good candidate arm std::unique_ptr<SFS::AppFile> packageArm; std::ignore = SFS::AppFile::Make( @@ -595,3 +621,43 @@ TEST_CASE("MSStoreDownloadFlow_Fail_Licensing_Forbidden", "[MSStoreDownloadFlow] REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN); INFO(downloadOutput.str()); } + +TEST_CASE("MSStoreDownloadFlow_Success_TargetOSVersion", "[MSStoreDownloadFlow][workflow]") +{ + TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideDownloadInstallerFileForMSStoreDownload(context); + TestHook::SetDisplayCatalogHttpPipelineStage_Override displayCatalogOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestDisplayCatalogResponse)); + TestHook::SetSfsClientAppContents_Override sfsClientOverride({ &GetSfsAppContentsOverrideFunction }); + TestHook::SetLicensingHttpPipelineStage_Override licensingOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestLicensingResponse)); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("DownloadFlowTest_MSStore.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::DownloadDirectory, tempDirectory); + context.Args.AddArg(Execution::Args::Type::Locale, "en-US"sv); + context.Args.AddArg(Execution::Args::Type::Platform, "Windows.Desktop"sv); + context.Args.AddArg(Execution::Args::Type::OSVersion, "9.0.0.0"sv); + + DownloadCommand download({}); + download.Execute(context); + REQUIRE(context.GetTerminationHR() == S_OK); + INFO(downloadOutput.str()); + + // Verify downloaded files + REQUIRE(std::filesystem::exists(tempDirectory.GetPath())); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_0.9.3.4_Universal_X64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx")); + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_0.9.0.0_Desktop_X64.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx")); + REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx")); + + // Verify license + REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml")); + std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"); + REQUIRE(licenseFile.is_open()); + std::string licenseFileStr; + std::getline(licenseFile, licenseFileStr); + REQUIRE(licenseFileStr == LicenseContent); +} diff --git a/src/AppInstallerCommonCore/MSStoreDownload.cpp b/src/AppInstallerCommonCore/MSStoreDownload.cpp @@ -651,6 +651,34 @@ namespace AppInstaller::MSStore return Manifest::PlatformEnum::Unknown; } + // Parses a string of the form `<PLATFORM>=<MINIMUM REQUIRED VERSION>{,}?`. + struct PlatformApplicability + { + explicit PlatformApplicability(std::string_view input, bool extractVersion = true) : + Platform(ConvertFromSfsPlatform(input)) + { + if (extractVersion) + { + THROW_HR_IF(E_INVALIDARG, input.empty()); + + size_t position = input.find('='); + THROW_HR_IF(E_INVALIDARG, std::string_view::npos == position); + + position += 1; + size_t length = input.size() - position; + if (length > 0 && input.back() == ',') + { + length -= 1; + } + + MinimumVersion = Utility::UInt64Version{ std::string{ input.substr(position, length) } }; + } + } + + Manifest::PlatformEnum Platform; + std::optional<Utility::UInt64Version> MinimumVersion; + }; + Utility::Architecture ConvertFromSfsArchitecture(SFS::Architecture sfsArchitecture) { switch (sfsArchitecture) @@ -670,17 +698,43 @@ namespace AppInstaller::MSStore return Utility::Architecture::Unknown; } - std::vector<Manifest::PlatformEnum> GetSfsPackageFileSupportedPlatforms(const SFS::AppFile& appFile, Manifest::PlatformEnum requiredPlatform) + std::vector<Manifest::PlatformEnum> GetSfsPackageFileSupportedPlatforms( + const SFS::AppFile& appFile, + Manifest::PlatformEnum requiredPlatform, + const std::optional<Utility::UInt64Version>& targetOSVersion) { std::vector<Manifest::PlatformEnum> supportedPlatforms; for (auto const& applicability : appFile.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) { - auto platform = ConvertFromSfsPlatform(applicability); - if (platform != Manifest::PlatformEnum::Unknown && - (platform == requiredPlatform || requiredPlatform == Manifest::PlatformEnum::Unknown)) + AICLI_LOG(Core, Verbose, << " examining platform [" << applicability << "] for applicability..."); + PlatformApplicability platform(applicability, targetOSVersion.has_value()); + + if (platform.Platform == Manifest::PlatformEnum::Unknown) { - supportedPlatforms.emplace_back(platform); + AICLI_LOG(Core, Verbose, << " not applicable due to unknown platform"); + continue; + } + + if (platform.MinimumVersion && targetOSVersion) + { + if (targetOSVersion.value() < platform.MinimumVersion.value()) + { + AICLI_LOG(Core, Verbose, << " not applicable due to OS version; target [" + << targetOSVersion.value().ToString() << "] is lower than minimum [" + << platform.MinimumVersion.value().ToString() << "]"); + continue; + } + } + + if (platform.Platform == requiredPlatform || requiredPlatform == Manifest::PlatformEnum::Unknown) + { + AICLI_LOG(Core, Verbose, << " applicable"); + supportedPlatforms.emplace_back(platform.Platform); + } + else + { + AICLI_LOG(Core, Verbose, << " not applicable due to platform requirement"); } } @@ -709,27 +763,22 @@ namespace AppInstaller::MSStore return supportedArchitectures; } - // This also checks if the file type is supported. If not supported, the return is empty string. std::string GetSfsPackageFileExtension(const SFS::AppFile& appFile) { - std::string fileExtension = std::filesystem::path{ appFile.GetFileId() }.extension().u8string(); + return std::filesystem::path{ appFile.GetFileId() }.extension().u8string(); + } - bool fileTypeSupported = false; + bool IsFileExtensionSupported(std::string_view fileExtension) + { for (auto const& supportedFileType : SupportedFileTypes) { if (Utility::CaseInsensitiveEquals(supportedFileType, fileExtension)) { - fileTypeSupported = true; - break; + return true; } } - if (!fileTypeSupported) - { - return {}; - } - - return fileExtension; + return false; } // The file name will be {Name}_{Version}_{Platform list}_{Arch list}.{File Extension} @@ -824,7 +873,8 @@ namespace AppInstaller::MSStore std::vector<MSStoreDownloadFile> PopulateSfsAppFileToMSStoreDownloadFileVector( const std::vector<SFS::AppFile>& appFiles, Utility::Architecture requiredArchitecture = Utility::Architecture::Unknown, - Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown) + Manifest::PlatformEnum requiredPlatform = Manifest::PlatformEnum::Unknown, + const std::optional<Utility::UInt64Version>& targetOSVersion = std::nullopt) { using PlatformAndArchitectureKey = std::pair<Manifest::PlatformEnum, Utility::Architecture>; @@ -834,23 +884,25 @@ namespace AppInstaller::MSStore for (auto const& appFile : appFiles) { + AICLI_LOG(Core, Info, << "Examining package [" << appFile.GetFileMoniker() << " (" << appFile.GetFileId() << ")] for download..."); + // Filter out unsupported packages - auto supportedPlatforms = GetSfsPackageFileSupportedPlatforms(appFile, requiredPlatform); + auto supportedPlatforms = GetSfsPackageFileSupportedPlatforms(appFile, requiredPlatform, targetOSVersion); if (supportedPlatforms.empty()) { - AICLI_LOG(Core, Info, << "Package skipped due to unsupported platforms. FileId:" << appFile.GetFileId()); + AICLI_LOG(Core, Verbose, << " package has no applicable platform."); continue; } auto supportedArchitectures = GetSfsPackageFileSupportedArchitectures(appFile, requiredArchitecture); if (supportedArchitectures.empty()) { - AICLI_LOG(Core, Info, << "Package skipped due to unsupported architecture. FileId:" << appFile.GetFileId()); + AICLI_LOG(Core, Verbose, << " package has no applicable architecture."); continue; } std::string fileExtension = GetSfsPackageFileExtension(appFile); - if (fileExtension.empty()) + if (!IsFileExtensionSupported(fileExtension)) { - AICLI_LOG(Core, Info, << "Package skipped due to unsupported file type. FileId:" << appFile.GetFileId()); + AICLI_LOG(Core, Verbose, << " package has unsupported file type [" << fileExtension << "]."); continue; } @@ -895,9 +947,15 @@ namespace AppInstaller::MSStore return result; } - MSStoreDownloadInfo CallSfsClientAndGetMSStoreDownloadInfo(std::string_view wuCategoryId, Utility::Architecture requiredArchitecture, Manifest::PlatformEnum requiredPlatform) + MSStoreDownloadInfo CallSfsClientAndGetMSStoreDownloadInfo( + std::string_view wuCategoryId, + Utility::Architecture requiredArchitecture, + Manifest::PlatformEnum requiredPlatform, + const std::optional<Utility::UInt64Version>& targetOSVersion) { - AICLI_LOG(Core, Info, << "CallSfsClientAndGetMSStoreDownloadInfo with WuCategoryId: " << wuCategoryId << " Architecture: " << Utility::ToString(requiredArchitecture) << " Platform: " << Manifest::PlatformToString(requiredPlatform)); + AICLI_LOG(Core, Info, << "CallSfsClientAndGetMSStoreDownloadInfo with WuCategoryId: " << wuCategoryId + << " Architecture: " << Utility::ToString(requiredArchitecture) << " Platform: " << Manifest::PlatformToString(requiredPlatform) + << " Target OS Version: " << (targetOSVersion ? targetOSVersion.value().ToString() : "any")); std::vector<SFS::AppContent> appContents; @@ -941,13 +999,13 @@ namespace AppInstaller::MSStore const auto& appContent = appContents.at(0); // Populate main packages - result.MainPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(appContent.GetFiles(), requiredArchitecture, requiredPlatform); + result.MainPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(appContent.GetFiles(), requiredArchitecture, requiredPlatform, targetOSVersion); // Populate dependency packages for (auto const& dependencyEntry : appContent.GetPrerequisites()) { // Not passing in required platform for dependencies. Dependencies are mostly Windows.Universal. - auto dependencyPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(dependencyEntry.GetFiles(), requiredArchitecture); + auto dependencyPackages = PopulateSfsAppFileToMSStoreDownloadFileVector(dependencyEntry.GetFiles(), requiredArchitecture, Manifest::PlatformEnum::Unknown, targetOSVersion); std::move(dependencyPackages.begin(), dependencyPackages.end(), std::inserter(result.DependencyPackages, result.DependencyPackages.end())); } @@ -1111,11 +1169,16 @@ namespace AppInstaller::MSStore } } + void MSStoreDownloadContext::TargetOSVersion(std::optional<Utility::UInt64Version> targetOSVersion) + { + m_targetOSVersion = std::move(targetOSVersion); + } + MSStoreDownloadInfo MSStoreDownloadContext::GetDownloadInfo() { #ifndef WINGET_DISABLE_FOR_FUZZING auto displayCatalogPackage = DisplayCatalogDetails::CallDisplayCatalogAndGetPreferredPackage(m_productId, m_locale, m_architecture, GetAuthHeaders(m_displayCatalogAuthenticator)); - auto downloadInfo = SfsClientDetails::CallSfsClientAndGetMSStoreDownloadInfo(displayCatalogPackage.WuCategoryId, m_architecture, m_platform); + auto downloadInfo = SfsClientDetails::CallSfsClientAndGetMSStoreDownloadInfo(displayCatalogPackage.WuCategoryId, m_architecture, m_platform, m_targetOSVersion); downloadInfo.ContentId = displayCatalogPackage.ContentId; return downloadInfo; #else diff --git a/src/AppInstallerCommonCore/Public/winget/MSStoreDownload.h b/src/AppInstallerCommonCore/Public/winget/MSStoreDownload.h @@ -41,6 +41,8 @@ namespace AppInstaller::MSStore std::string locale, AppInstaller::Authentication::AuthenticationArguments authArgs); + void TargetOSVersion(std::optional<Utility::UInt64Version> targetOSVersion); + // Calls display catalog API and sfs-client to get download info. MSStoreDownloadInfo GetDownloadInfo(); @@ -51,6 +53,7 @@ namespace AppInstaller::MSStore std::string m_productId; AppInstaller::Utility::Architecture m_architecture = AppInstaller::Utility::Architecture::Unknown; AppInstaller::Manifest::PlatformEnum m_platform = AppInstaller::Manifest::PlatformEnum::Unknown; + std::optional<Utility::UInt64Version> m_targetOSVersion = std::nullopt; std::string m_locale; std::unique_ptr<AppInstaller::Authentication::Authenticator> m_displayCatalogAuthenticator; std::unique_ptr<AppInstaller::Authentication::Authenticator> m_licensingAuthenticator; diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -530,4 +530,17 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(WindowsPlatform value) + { + switch (value) + { + case WindowsPlatform::Unknown: return AppInstaller::Manifest::PlatformEnum::Unknown; + case WindowsPlatform::Universal: return AppInstaller::Manifest::PlatformEnum::Universal; + case WindowsPlatform::Desktop: return AppInstaller::Manifest::PlatformEnum::Desktop; + case WindowsPlatform::IoT: return AppInstaller::Manifest::PlatformEnum::IoT; + case WindowsPlatform::Team: return AppInstaller::Manifest::PlatformEnum::Team; + case WindowsPlatform::Holographic: return AppInstaller::Manifest::PlatformEnum::Holographic; + default: return AppInstaller::Manifest::PlatformEnum::Unknown; + } + } } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h @@ -33,6 +33,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation ::AppInstaller::Manifest::ScopeEnum GetManifestRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope scope); winrt::Microsoft::Management::Deployment::AddPackageCatalogStatus GetAddPackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); + ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::InstallResultStatus>) \ diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.cpp b/src/Microsoft.Management.Deployment/DownloadOptions.cpp @@ -105,6 +105,37 @@ namespace winrt::Microsoft::Management::Deployment::implementation void DownloadOptions::AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value) { m_authenticationArguments = value; - } + } + + bool DownloadOptions::SkipMicrosoftStoreLicense() + { + return m_skipMicrosoftStoreLicense; + } + + void DownloadOptions::SkipMicrosoftStoreLicense(bool value) + { + m_skipMicrosoftStoreLicense = value; + } + + winrt::Microsoft::Management::Deployment::WindowsPlatform DownloadOptions::Platform() + { + return m_platform; + } + + void DownloadOptions::Platform(winrt::Microsoft::Management::Deployment::WindowsPlatform value) + { + m_platform = value; + } + + hstring DownloadOptions::TargetOSVersion() + { + return hstring(m_targetOSVersion); + } + + void DownloadOptions::TargetOSVersion(hstring const& value) + { + m_targetOSVersion = value; + } + CoCreatableMicrosoftManagementDeploymentClass(DownloadOptions); } diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.h b/src/Microsoft.Management.Deployment/DownloadOptions.h @@ -33,6 +33,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation void CorrelationData(hstring const& value); winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); + bool SkipMicrosoftStoreLicense(); + void SkipMicrosoftStoreLicense(bool value); + winrt::Microsoft::Management::Deployment::WindowsPlatform Platform(); + void Platform(winrt::Microsoft::Management::Deployment::WindowsPlatform value); + hstring TargetOSVersion(); + void TargetOSVersion(hstring const& value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -47,6 +53,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool m_acceptPackageAgreements = true; std::wstring m_correlationData = L""; winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; + bool m_skipMicrosoftStoreLicense = false; + winrt::Microsoft::Management::Deployment::WindowsPlatform m_platform = winrt::Microsoft::Management::Deployment::WindowsPlatform::Unknown; + std::wstring m_targetOSVersion; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -690,6 +690,23 @@ namespace winrt::Microsoft::Management::Deployment::implementation context->Args.AddArg(Execution::Args::Type::AuthenticationMode, ::AppInstaller::Authentication::AuthenticationModeToString(GetAuthenticationMode(options.AuthenticationArguments().AuthenticationMode()))); context->Args.AddArg(Execution::Args::Type::AuthenticationAccount, ::AppInstaller::Utility::ConvertToUTF8(options.AuthenticationArguments().AuthenticationAccount())); } + + if (options.SkipMicrosoftStoreLicense()) + { + context->Args.AddArg(Execution::Args::Type::SkipMicrosoftStorePackageLicense); + } + + WindowsPlatform platform = options.Platform(); + if (platform != WindowsPlatform::Unknown) + { + context->Args.AddArg(Execution::Args::Type::Platform, AppInstaller::Manifest::PlatformToString(GetPlatformEnum(platform))); + } + + hstring targetOSVersion = options.TargetOSVersion(); + if (!targetOSVersion.empty()) + { + context->Args.AddArg(Execution::Args::Type::OSVersion, ::AppInstaller::Utility::ConvertToUTF8(targetOSVersion)); + } } } diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1159,6 +1159,24 @@ namespace Microsoft.Management.Deployment } } + /// The Windows platform type. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 13)] + enum WindowsPlatform + { + /// An unknown platform + Unknown, + /// Windows.Universal + Universal, + /// Windows.Desktop + Desktop, + /// Windows.IoT + IoT, + /// Windows.Team + Team, + /// Windows.Holographic + Holographic, + }; + /// Options when downloading a package. /// Intended to allow full compatibility with the "winget download" command line interface. [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] @@ -1203,6 +1221,18 @@ namespace Microsoft.Management.Deployment /// Authentication arguments used when downloading the package installer if authentication is required. AuthenticationArguments AuthenticationArguments; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 13)] + { + /// If the package is licensed from the Microsoft Store, setting this value to true will not attempt to download the license file. + Boolean SkipMicrosoftStoreLicense; + + /// The platform to download the package for. + WindowsPlatform Platform; + + /// When applicable, uses the provided value as the target OS version for the download. + String TargetOSVersion; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ExportPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ExportPackageCmdlet.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ExportPackageCmdlet.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -32,6 +32,24 @@ namespace Microsoft.WinGet.Client.Commands public string DownloadDirectory { get; set; } /// <summary> + /// Gets or sets a value indicating whether to skip acquiring the license from a Store package. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter SkipMicrosoftStoreLicense { get; set; } + + /// <summary> + /// Gets or sets the platform to download the package for. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public PSObjects.PSWindowsPlatform Platform { get; set; } + + /// <summary> + /// Gets or sets the target OS version to download for. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string TargetOSVersion { get; set; } + + /// <summary> /// Installs a package from the pipeline or from a configured source. /// </summary> protected override void ProcessRecord() @@ -48,7 +66,15 @@ namespace Microsoft.WinGet.Client.Commands this.AllowHashMismatch.ToBool(), this.SkipDependencies.ToBool(), this.Locale); - this.command.Download(this.DownloadDirectory, this.MatchOption.ToString(), this.Scope.ToString(), this.Architecture.ToString(), this.InstallerType.ToString()); + this.command.Download( + this.DownloadDirectory, + this.MatchOption.ToString(), + this.Scope.ToString(), + this.Architecture.ToString(), + this.InstallerType.ToString(), + this.SkipMicrosoftStoreLicense.ToBool(), + this.Platform.ToString(), + this.TargetOSVersion); } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSWindowsPlatform.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSWindowsPlatform.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSWindowsPlatform.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// <summary> + /// The Windows platform type. + /// </summary> + public enum PSWindowsPlatform + { + /// <summary> + /// Let winget decide. + /// </summary> + Default, + + /// <summary> + /// Windows.Universal + /// </summary> + Universal, + + /// <summary> + /// Windows.Desktop + /// </summary> + Desktop, + + /// <summary> + /// Windows.IoT + /// </summary> + IoT, + + /// <summary> + /// Windows.Team + /// </summary> + Team, + + /// <summary> + /// Windows.Holographic + /// </summary> + Holographic, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/DownloadCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/DownloadCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="DownloadCommand.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -92,12 +92,18 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="psPackageInstallScope">PSPackageInstallScope.</param> /// <param name="psProcessorArchitecture">PSProcessorArchitecture.</param> /// <param name="psPackageInstallerType">PSPackageInstallerType.</param> + /// <param name="skipMicrosoftStoreLicense">If true, skips downloading a Store license.</param> + /// <param name="psWindowsPlatform">The platform to download the package for.</param> + /// <param name="targetOSVersion">The target OS version to download for.</param> public void Download( string downloadDirectory, string psPackageFieldMatchOption, string psPackageInstallScope, string psProcessorArchitecture, - string psPackageInstallerType) + string psPackageInstallerType, + bool skipMicrosoftStoreLicense, + string psWindowsPlatform, + string targetOSVersion) { var result = this.Execute( async () => await this.GetPackageAndExecuteAsync( @@ -123,6 +129,17 @@ namespace Microsoft.WinGet.Client.Engine.Commands } options.Scope = PSEnumHelpers.ToPackageInstallScope(psPackageInstallScope); + options.SkipMicrosoftStoreLicense = skipMicrosoftStoreLicense; + + if (!PSEnumHelpers.IsDefaultEnum(psWindowsPlatform)) + { + options.Platform = PSEnumHelpers.ToWindowsPlatform(psWindowsPlatform); + } + + if (!string.IsNullOrEmpty(targetOSVersion)) + { + options.TargetOSVersion = targetOSVersion; + } return await this.DownloadPackageAsync(package, options); })); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs @@ -149,5 +149,24 @@ namespace Microsoft.WinGet.Client.Engine.Helpers _ => throw new InvalidOperationException(), }; } + + /// <summary> + /// Converts PSWindowsPlatform string value to WindowsPlatform. + /// </summary> + /// <param name="value">PSWindowsPlatform string value.</param> + /// <returns>WindowsPlatform.</returns> + public static WindowsPlatform ToWindowsPlatform(string value) + { + return value switch + { + "Default" => WindowsPlatform.Unknown, + "Universal" => WindowsPlatform.Universal, + "Desktop" => WindowsPlatform.Desktop, + "IoT" => WindowsPlatform.IoT, + "Team" => WindowsPlatform.Team, + "Holographic" => WindowsPlatform.Holographic, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -245,6 +245,6 @@ if (-not $SkipImportModule) foreach($module in $modules) { Write-Host "Importing module $($module.Name)" -ForegroundColor Green - Import-Module "$moduleRootOutput\$($module.Name)\" -Force + Import-Module "$moduleRootOutput\$($module.Name)\$($module.Name).psd1" -Force } }