Skip to content

Commit bc7a073

Browse files
authored
Download MS Store package for target OS (microsoft#5689)
Fixes microsoft#4996 ## Change Adds the ability to specify the target OS version for downloads from the Microsoft Store. This is achieved by filtering the set of applicable platforms in `GetSfsPackageFileSupportedPlatforms` further than the existing platform targeting. Now they must also have a minimum specified OS version >= the target OS version. `winget.exe download` has a new parameter `--os-version`. This is a UINT64 version (4 part version with 2^16-1 maximum value for each part) that should be given as the target OS version. COM `DownloadOptions` has a new property `TargetOSVersion`, which is a string of the same format as above. PowerShell `Export-WinGetPackage` has a new parameter `-TargetOSVersion`, which is also a string of the same format. Also added the previously implemented options to skip the license download (`--skip-license`) and the target platform (`--platform`) to COM (`SkipMicrosoftStoreLicense` and `Platform`) and PowerShell (`-SkipMicrosoftStoreLicense` and `-Platform`).
1 parent 9bb4aa4 commit bc7a073

File tree

21 files changed

+489
-127
lines changed

21 files changed

+489
-127
lines changed

src/AppInstallerCLI.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ VisualStudioVersion = 17.2.32630.192
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "AppInstallerCLIPackage", "AppInstallerCLIPackage\AppInstallerCLIPackage.wapproj", "{6AA3791A-0713-4548-A357-87A323E7AC3A}"
77
ProjectSection(ProjectDependencies) = postProject
8+
{0BA531C8-CF0C-405B-8221-0FE51BA529D1} = {0BA531C8-CF0C-405B-8221-0FE51BA529D1}
89
{1CC41A9A-AE66-459D-9210-1E572DD7BE69} = {1CC41A9A-AE66-459D-9210-1E572DD7BE69}
910
{2B00D362-AC92-41F3-A8D2-5B1599BDCA01} = {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}
11+
{33745E4A-39E2-676F-7E23-50FB43848D25} = {33745E4A-39E2-676F-7E23-50FB43848D25}
1012
{5B6F90DF-FD19-4BAE-83D9-24DAD128E777} = {5B6F90DF-FD19-4BAE-83D9-24DAD128E777}
1113
{6597EB04-D105-49A7-A5A3-D27FE1DF895E} = {6597EB04-D105-49A7-A5A3-D27FE1DF895E}
1214
{CA460806-5E41-4E97-9A3D-1D74B433B663} = {CA460806-5E41-4E97-9A3D-1D74B433B663}
@@ -215,6 +217,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E
215217
EndProject
216218
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetMCPServer", "WinGetMCPServer\WinGetMCPServer.csproj", "{33745E4A-39E2-676F-7E23-50FB43848D25}"
217219
EndProject
220+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F49C4C89-447E-4D15-B38B-5A8DCFB134AF}"
221+
ProjectSection(SolutionItems) = preProject
222+
PowerShell\scripts\Execute-WinGetTests.ps1 = PowerShell\scripts\Execute-WinGetTests.ps1
223+
PowerShell\scripts\Initialize-LocalWinGetModules.ps1 = PowerShell\scripts\Initialize-LocalWinGetModules.ps1
224+
EndProjectSection
225+
EndProject
218226
Global
219227
GlobalSection(SolutionConfigurationPlatforms) = preSolution
220228
Debug|ARM64 = Debug|ARM64
@@ -1046,6 +1054,7 @@ Global
10461054
{A0B4F808-B190-41C4-97CB-C8EA1932F84F} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
10471055
{A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995}
10481056
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
1057+
{F49C4C89-447E-4D15-B38B-5A8DCFB134AF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
10491058
EndGlobalSection
10501059
GlobalSection(ExtensibilityGlobals) = postSolution
10511060
SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857}

src/AppInstallerCLICore/Argument.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ namespace AppInstaller::CLI
265265
return { type, "platform"_liv, ArgTypeCategory::None };
266266
case Execution::Args::Type::SkipMicrosoftStorePackageLicense:
267267
return { type, "skip-microsoft-store-package-license"_liv, "skip-license"_liv, ArgTypeCategory::None };
268+
case Execution::Args::Type::OSVersion:
269+
return { type, "os-version"_liv, ArgTypeCategory::None };
268270

269271
// Common arguments
270272
case Execution::Args::Type::NoVT:
Lines changed: 95 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,59 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT License.
3-
#include "pch.h"
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
#include "pch.h"
44
#include "DownloadCommand.h"
5-
#include "Workflows/CompletionFlow.h"
6-
#include "Workflows/DownloadFlow.h"
7-
#include "Workflows/InstallFlow.h"
8-
#include "Workflows/PromptFlow.h"
9-
#include "Resources.h"
5+
#include "Workflows/CompletionFlow.h"
6+
#include "Workflows/DownloadFlow.h"
7+
#include "Workflows/InstallFlow.h"
8+
#include "Workflows/PromptFlow.h"
9+
#include "Resources.h"
1010
#include <AppInstallerRuntime.h>
11-
#include <winget/ManifestCommon.h>
12-
13-
namespace AppInstaller::CLI
14-
{
15-
using namespace AppInstaller::CLI::Execution;
16-
using namespace AppInstaller::CLI::Workflow;
17-
using namespace AppInstaller::Utility::literals;
18-
19-
std::vector<Argument> DownloadCommand::GetArguments() const
20-
{
21-
return {
22-
Argument::ForType(Args::Type::Query),
23-
Argument::ForType(Args::Type::DownloadDirectory),
24-
Argument::ForType(Args::Type::Manifest),
25-
Argument::ForType(Args::Type::Id),
26-
Argument::ForType(Args::Type::Name),
27-
Argument::ForType(Args::Type::Moniker),
28-
Argument::ForType(Args::Type::Version),
29-
Argument::ForType(Args::Type::Channel),
30-
Argument::ForType(Args::Type::Source),
31-
Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help },
32-
Argument::ForType(Args::Type::InstallerArchitecture),
33-
Argument::ForType(Args::Type::InstallerType),
34-
Argument::ForType(Args::Type::Exact),
35-
Argument::ForType(Args::Type::Locale),
36-
Argument::ForType(Args::Type::HashOverride),
37-
Argument::ForType(Args::Type::SkipDependencies),
38-
Argument::ForType(Args::Type::CustomHeader),
39-
Argument::ForType(Args::Type::AuthenticationMode),
40-
Argument::ForType(Args::Type::AuthenticationAccount),
41-
Argument::ForType(Args::Type::AcceptPackageAgreements),
11+
#include <winget/ManifestCommon.h>
12+
13+
namespace AppInstaller::CLI
14+
{
15+
using namespace AppInstaller::CLI::Execution;
16+
using namespace AppInstaller::CLI::Workflow;
17+
using namespace AppInstaller::Utility::literals;
18+
19+
std::vector<Argument> DownloadCommand::GetArguments() const
20+
{
21+
return {
22+
Argument::ForType(Args::Type::Query),
23+
Argument::ForType(Args::Type::DownloadDirectory),
24+
Argument::ForType(Args::Type::Manifest),
25+
Argument::ForType(Args::Type::Id),
26+
Argument::ForType(Args::Type::Name),
27+
Argument::ForType(Args::Type::Moniker),
28+
Argument::ForType(Args::Type::Version),
29+
Argument::ForType(Args::Type::Channel),
30+
Argument::ForType(Args::Type::Source),
31+
Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help },
32+
Argument::ForType(Args::Type::InstallerArchitecture),
33+
Argument::ForType(Args::Type::InstallerType),
34+
Argument::ForType(Args::Type::Exact),
35+
Argument::ForType(Args::Type::Locale),
36+
Argument::ForType(Args::Type::HashOverride),
37+
Argument::ForType(Args::Type::SkipDependencies),
38+
Argument::ForType(Args::Type::CustomHeader),
39+
Argument::ForType(Args::Type::AuthenticationMode),
40+
Argument::ForType(Args::Type::AuthenticationAccount),
41+
Argument::ForType(Args::Type::AcceptPackageAgreements),
4242
Argument::ForType(Args::Type::AcceptSourceAgreements),
4343
Argument::ForType(Args::Type::SkipMicrosoftStorePackageLicense),
44-
Argument::ForType(Args::Type::Platform),
45-
};
46-
}
47-
48-
Resource::LocString DownloadCommand::ShortDescription() const
49-
{
50-
return { Resource::String::DownloadCommandShortDescription };
51-
}
52-
53-
Resource::LocString DownloadCommand::LongDescription() const
54-
{
55-
return { Resource::String::DownloadCommandLongDescription };
44+
Argument::ForType(Args::Type::Platform),
45+
Argument{ Args::Type::OSVersion, Resource::String::OSVersionDescription, ArgumentType::Standard, Argument::Visibility::Help },
46+
};
47+
}
48+
49+
Resource::LocString DownloadCommand::ShortDescription() const
50+
{
51+
return { Resource::String::DownloadCommandShortDescription };
52+
}
53+
54+
Resource::LocString DownloadCommand::LongDescription() const
55+
{
56+
return { Resource::String::DownloadCommandLongDescription };
5657
}
5758

5859
void DownloadCommand::Complete(Context& context, Args::Type valueType) const
@@ -82,15 +83,15 @@ namespace AppInstaller::CLI
8283
// Intentionally output nothing to allow pass through to filesystem.
8384
break;
8485
}
85-
}
86-
87-
Utility::LocIndView DownloadCommand::HelpLink() const
88-
{
89-
return "https://aka.ms/winget-command-download"_liv;
90-
}
91-
92-
void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const
93-
{
86+
}
87+
88+
Utility::LocIndView DownloadCommand::HelpLink() const
89+
{
90+
return "https://aka.ms/winget-command-download"_liv;
91+
}
92+
93+
void DownloadCommand::ValidateArgumentsInternal(Args& execArgs) const
94+
{
9495
Argument::ValidateCommonArguments(execArgs);
9596

9697
if (execArgs.Contains(Execution::Args::Type::Platform))
@@ -103,39 +104,39 @@ namespace AppInstaller::CLI
103104
});
104105
throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::Platform).Name(), validOptions));
105106
}
106-
}
107-
}
108-
109-
void DownloadCommand::ExecuteInternal(Context& context) const
110-
{
107+
}
108+
}
109+
110+
void DownloadCommand::ExecuteInternal(Context& context) const
111+
{
111112
context.SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly);
112113

113-
context << Workflow::InitializeInstallerDownloadAuthenticatorsMap;
114-
115-
if (context.Args.Contains(Execution::Args::Type::Manifest))
116-
{
117-
context <<
118-
Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
119-
Workflow::GetManifestFromArg;
120-
}
121-
else
122-
{
123-
context <<
124-
Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
125-
Workflow::OpenSource() <<
126-
Workflow::SearchSourceForSingle <<
127-
Workflow::HandleSearchResultFailures <<
128-
Workflow::EnsureOneMatchFromSearchResult(OperationType::Download) <<
129-
Workflow::GetManifestFromPackage(false);
130-
}
131-
132-
context <<
133-
Workflow::SetDownloadDirectory <<
134-
Workflow::SelectInstaller <<
135-
Workflow::EnsureApplicableInstaller <<
136-
Workflow::ReportIdentityAndInstallationDisclaimer <<
137-
Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) <<
138-
Workflow::DownloadPackageDependencies <<
139-
Workflow::DownloadInstaller;
140-
}
141-
}
114+
context << Workflow::InitializeInstallerDownloadAuthenticatorsMap;
115+
116+
if (context.Args.Contains(Execution::Args::Type::Manifest))
117+
{
118+
context <<
119+
Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
120+
Workflow::GetManifestFromArg;
121+
}
122+
else
123+
{
124+
context <<
125+
Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
126+
Workflow::OpenSource() <<
127+
Workflow::SearchSourceForSingle <<
128+
Workflow::HandleSearchResultFailures <<
129+
Workflow::EnsureOneMatchFromSearchResult(OperationType::Download) <<
130+
Workflow::GetManifestFromPackage(false);
131+
}
132+
133+
context <<
134+
Workflow::SetDownloadDirectory <<
135+
Workflow::SelectInstaller <<
136+
Workflow::EnsureApplicableInstaller <<
137+
Workflow::ReportIdentityAndInstallationDisclaimer <<
138+
Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) <<
139+
Workflow::DownloadPackageDependencies <<
140+
Workflow::DownloadInstaller;
141+
}
142+
}

src/AppInstallerCLICore/ExecutionArgs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ namespace AppInstaller::CLI::Execution
9191
DownloadDirectory,
9292
SkipMicrosoftStorePackageLicense,
9393
Platform,
94+
OSVersion,
9495

9596
// Setting Command
9697
AdminSettingEnable,

src/AppInstallerCLICore/Resources.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ namespace AppInstaller::CLI::Resource
491491
WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp);
492492
WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined);
493493
WINGET_DEFINE_RESOURCE_STRINGID(Options);
494+
WINGET_DEFINE_RESOURCE_STRINGID(OSVersionDescription);
494495
WINGET_DEFINE_RESOURCE_STRINGID(OutputDirectoryArgumentDescription);
495496
WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription);
496497
WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription);

src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@ namespace AppInstaller::CLI::Workflow
293293

294294
MSStoreDownloadContext downloadContext{ installer.ProductId, requiredArchitecture, requiredPlatform, requiredLocale, GetAuthenticationArguments(context) };
295295

296+
if (context.Args.Contains(Execution::Args::Type::OSVersion))
297+
{
298+
Utility::UInt64Version targetOSVersion{ std::string{ context.Args.GetArg(Execution::Args::Type::OSVersion) } };
299+
downloadContext.TargetOSVersion(std::move(targetOSVersion));
300+
}
301+
296302
MSStoreDownloadInfo downloadInfo;
297303
try
298304
{

src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3387,4 +3387,7 @@ An unlocalized JSON fragment will follow on another line.</comment>
33873387
<data name="PolicyEnableMcpServer" xml:space="preserve">
33883388
<value>Enable Windows Package Manager MCP Server</value>
33893389
</data>
3390+
<data name="OSVersionDescription" xml:space="preserve">
3391+
<value>Target OS version</value>
3392+
</data>
33903393
</root>

src/AppInstallerCLITests/MSStoreDownloadFlow.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ std::vector<SFS::AppContent> GetSfsAppContentsOverrideFunction(std::string_view
133133
dependencyX64);
134134
dependencyPackages.emplace_back(std::move(*dependencyX64));
135135

136+
// Lower target OS dependency
137+
std::unique_ptr<SFS::AppFile> dependencyX64_lower;
138+
std::ignore = SFS::AppFile::Make(
139+
wuCategoryIdStr + ".appx",
140+
"https://NotUsed/" + wuCategoryIdStr + "/dependency/x64",
141+
100,
142+
{ { SFS::HashType::Sha256, base64EncodedSha256 } },
143+
{ SFS::Architecture::Amd64 },
144+
{ "Universal=9.0.0.0" },
145+
wuCategoryIdStr + ".Dependency_0.9.3.4_x64__8wekyb3d8bbwe",
146+
dependencyX64_lower);
147+
dependencyPackages.emplace_back(std::move(*dependencyX64_lower));
148+
136149
std::unique_ptr<SFS::AppFile> dependencyArm;
137150
std::ignore = SFS::AppFile::Make(
138151
wuCategoryIdStr + ".appx",
@@ -166,6 +179,19 @@ std::vector<SFS::AppContent> GetSfsAppContentsOverrideFunction(std::string_view
166179
packageX64);
167180
packages.emplace_back(std::move(*packageX64));
168181

182+
// Good candidate x64, lower minimum OS version, lower package version
183+
std::unique_ptr<SFS::AppFile> packageX64_lower;
184+
std::ignore = SFS::AppFile::Make(
185+
wuCategoryIdStr + ".appx",
186+
"https://NotUsed/" + wuCategoryIdStr + "/x64",
187+
100,
188+
{ { SFS::HashType::Sha256, base64EncodedSha256 } },
189+
{ SFS::Architecture::Amd64 },
190+
{ "Desktop=9.0.0.0" },
191+
wuCategoryIdStr + "_0.9.0.0_x64__8wekyb3d8bbwe",
192+
packageX64_lower);
193+
packages.emplace_back(std::move(*packageX64_lower));
194+
169195
// Good candidate arm
170196
std::unique_ptr<SFS::AppFile> packageArm;
171197
std::ignore = SFS::AppFile::Make(
@@ -595,3 +621,43 @@ TEST_CASE("MSStoreDownloadFlow_Fail_Licensing_Forbidden", "[MSStoreDownloadFlow]
595621
REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN);
596622
INFO(downloadOutput.str());
597623
}
624+
625+
TEST_CASE("MSStoreDownloadFlow_Success_TargetOSVersion", "[MSStoreDownloadFlow][workflow]")
626+
{
627+
TestCommon::TempDirectory tempDirectory("TestDownloadDirectory", false);
628+
629+
std::ostringstream downloadOutput;
630+
TestContext context{ downloadOutput, std::cin };
631+
auto previousThreadGlobals = context.SetForCurrentThread();
632+
OverrideDownloadInstallerFileForMSStoreDownload(context);
633+
TestHook::SetDisplayCatalogHttpPipelineStage_Override displayCatalogOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestDisplayCatalogResponse));
634+
TestHook::SetSfsClientAppContents_Override sfsClientOverride({ &GetSfsAppContentsOverrideFunction });
635+
TestHook::SetLicensingHttpPipelineStage_Override licensingOverride(GetTestRestRequestHandler(web::http::status_codes::OK, TestLicensingResponse));
636+
context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("DownloadFlowTest_MSStore.yaml").GetPath().u8string());
637+
context.Args.AddArg(Execution::Args::Type::DownloadDirectory, tempDirectory);
638+
context.Args.AddArg(Execution::Args::Type::Locale, "en-US"sv);
639+
context.Args.AddArg(Execution::Args::Type::Platform, "Windows.Desktop"sv);
640+
context.Args.AddArg(Execution::Args::Type::OSVersion, "9.0.0.0"sv);
641+
642+
DownloadCommand download({});
643+
download.Execute(context);
644+
REQUIRE(context.GetTerminationHR() == S_OK);
645+
INFO(downloadOutput.str());
646+
647+
// Verify downloaded files
648+
REQUIRE(std::filesystem::exists(tempDirectory.GetPath()));
649+
REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies"));
650+
REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_0.9.3.4_Universal_X64.appx"));
651+
REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"Dependencies" / L"TestCategoryIdEnglish.Dependency_1.2.3.4_Universal_Arm.appx"));
652+
REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_0.9.0.0_Desktop_X64.appx"));
653+
REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish_1.0.0.0_Desktop_Arm.appx"));
654+
REQUIRE_FALSE(std::filesystem::exists(tempDirectory.GetPath() / L"TestCategoryIdEnglish.IoT_2.0.0.0_IoT_Arm.appx"));
655+
656+
// Verify license
657+
REQUIRE(std::filesystem::exists(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml"));
658+
std::ifstream licenseFile(tempDirectory.GetPath() / L"9WZDNCRFJ364_License.xml");
659+
REQUIRE(licenseFile.is_open());
660+
std::string licenseFileStr;
661+
std::getline(licenseFile, licenseFileStr);
662+
REQUIRE(licenseFileStr == LicenseContent);
663+
}

0 commit comments

Comments
 (0)