Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/AppInstallerCLI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
189 changes: 95 additions & 94 deletions src/AppInstallerCLICore/Commands/DownloadCommand.cpp
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/ExecutionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ namespace AppInstaller::CLI::Execution
DownloadDirectory,
SkipMicrosoftStorePackageLicense,
Platform,
OSVersion,

// Setting Command
AdminSettingEnable,
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -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>
66 changes: 66 additions & 0 deletions src/AppInstallerCLITests/MSStoreDownloadFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
Loading