From dfaa17efebd87c987de4816882fe1dba3aa2e42b Mon Sep 17 00:00:00 2001
From: CatBraaain <84499939+CatBraaain@users.noreply.github.com>
Date: Sun, 5 Jul 2026 13:22:59 +0900
Subject: [PATCH] feat: add `--ignore-unavailable` option to install command
---
src/AppInstallerCLICore/Argument.cpp | 2 ++
.../Commands/ImportCommand.cpp | 2 +-
.../Commands/InstallCommand.cpp | 1 +
src/AppInstallerCLICore/Resources.h | 2 +-
.../Shared/Strings/en-us/winget.resw | 2 +-
src/AppInstallerCLITests/InstallFlow.cpp | 25 ++++++++++++++++++-
6 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp
index 2de195e6ff..c9de94dad0 100644
--- a/src/AppInstallerCLICore/Argument.cpp
+++ b/src/AppInstallerCLICore/Argument.cpp
@@ -420,6 +420,8 @@ namespace AppInstaller::CLI
return Argument{ type, Resource::String::DependenciesOnlyArgumentDescription, ArgumentType::Flag, false };
case Args::Type::IgnoreLocalArchiveMalwareScan:
return Argument{ type, Resource::String::IgnoreLocalArchiveMalwareScanArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::LocalArchiveMalwareScanOverride, Settings::BoolAdminSetting::LocalArchiveMalwareScanOverride };
+ case Args::Type::IgnoreUnavailable:
+ return Argument{ type, Resource::String::IgnoreUnavailableArgumentDescription, ArgumentType::Flag };
case Args::Type::SourceName:
return Argument{ type, Resource::String::SourceNameArgumentDescription, ArgumentType::Positional, false };
case Args::Type::SourceArg:
diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp
index 39e4d9b4ab..ec8c638f1c 100644
--- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp
@@ -17,7 +17,7 @@ namespace AppInstaller::CLI
{
return {
Argument{ Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true },
- Argument{ Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag },
+ Argument::ForType(Execution::Args::Type::IgnoreUnavailable),
Argument{ Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag },
Argument::ForType(Execution::Args::Type::NoUpgrade),
Argument::ForType(Execution::Args::Type::AcceptPackageAgreements),
diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp
index e96139186e..ab52f9076c 100644
--- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp
@@ -58,6 +58,7 @@ namespace AppInstaller::CLI
Argument::ForType(Args::Type::UninstallPrevious),
Argument::ForType(Args::Type::Force),
Argument{ Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag, Argument::Visibility::Hidden},
+ Argument::ForType(Args::Type::IgnoreUnavailable),
};
}
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index b35f9b3dc9..175ad24c8c 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -353,6 +353,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(IdArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IgnoreLocalArchiveMalwareScanArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IgnoreResumeLimitArgumentDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(IgnoreUnavailableArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IgnoreWarningsArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandReportDependencies);
@@ -360,7 +361,6 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ImportFileHasInvalidSchema);
WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnorePackageVersionsArgumentDescription);
- WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed);
WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled);
WINGET_DEFINE_RESOURCE_STRINGID(IncludePinnedArgumentDescription);
diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index b562e3022a..7ce1edd567 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -894,7 +894,7 @@ They can be configured through the settings file 'winget settings'.
Package is already installed: {0}
{Locked="{0}"} Message displayed to inform the user that an application package is already installed. {0} is a placeholder replaced by the package identifier or search query.
-
+
Ignore unavailable packages
diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp
index 64fb767bbc..88e1a8f89f 100644
--- a/src/AppInstallerCLITests/InstallFlow.cpp
+++ b/src/AppInstallerCLITests/InstallFlow.cpp
@@ -234,7 +234,7 @@ TEST_CASE("InstallFlow_UnsupportedArguments_Error", "[InstallFlow][workflow]")
install.Execute(context);
INFO(installOutput.str());
- // Verify unsupported arguments error message is shown
+ // Verify unsupported arguments error message is shown
REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UNSUPPORTED_ARGUMENT);
REQUIRE(!std::filesystem::exists(installResultPath.GetPath()));
REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::UnsupportedArgument).get()) != std::string::npos);
@@ -1343,6 +1343,29 @@ TEST_CASE("InstallFlow_InstallMultiple_SearchFailed", "[InstallFlow][workflow][M
REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_QUERIES_FOUND_SINGLE);
}
+TEST_CASE("InstallFlow_InstallMultiple_IgnoreUnavailable", "[InstallFlow][workflow][MultiQuery]")
+{
+ TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt");
+
+ std::ostringstream installOutput;
+ TestContext context{ installOutput, std::cin };
+ auto previousThreadGlobals = context.SetForCurrentThread();
+ OverrideForShellExecute(context);
+ OverrideForOpenSource(context, CreateTestSource({ TSR::TestInstaller_Exe }), true);
+ context.Args.AddArg(Execution::Args::Type::MultiQuery, TSR::TestInstaller_Exe.Query);
+ context.Args.AddArg(Execution::Args::Type::MultiQuery, TSR::TestInstaller_Msix.Query);
+ context.Args.AddArg(Execution::Args::Type::IgnoreUnavailable);
+
+ InstallCommand installCommand({});
+ installCommand.Execute(context);
+ INFO(installOutput.str());
+
+ // Verify the available package was installed despite the missing one,
+ // and the unavailable package was reported as not found.
+ REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath()));
+ REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::MultiQueryPackageNotFound(LocIndString{ TSR::TestInstaller_Msix.Query })).get()) != std::string::npos);
+}
+
TEST_CASE("InstallFlow_InstallAcquiresLock", "[InstallFlow][workflow]")
{
TestCommon::TempFile installResultPath("TestExeInstalled.txt");