From 0797990ea5f781065b707bca4dde059841323f66 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
---
doc/ReleaseNotes.md | 4 ++-
doc/windows/package-manager/winget/install.md | 1 +
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 ++++++++++++++++++-
8 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md
index 28f90d9b5d..a014b5c61e 100644
--- a/doc/ReleaseNotes.md
+++ b/doc/ReleaseNotes.md
@@ -1,6 +1,8 @@
## New in v1.29
-Nothing yet.
+### `--ignore-unavailable` flag for `install`
+
+Added a new `--ignore-unavailable` flag to the `install` command. When installing multiple packages, this flag allows the operation to continue with the remaining packages instead of failing entirely when one or more packages are not found in the configured sources. This brings the same behavior previously available with `import --ignore-unavailable` to direct multi-package installs.
## Bug Fixes
diff --git a/doc/windows/package-manager/winget/install.md b/doc/windows/package-manager/winget/install.md
index 9973a82626..9d8a620544 100644
--- a/doc/windows/package-manager/winget/install.md
+++ b/doc/windows/package-manager/winget/install.md
@@ -56,6 +56,7 @@ The options allow you to customize the install experience to meet your needs.
| **--ignore-security-hash** | Ignore the installer hash check failure |
| **--skip-dependencies** | Skip processing package dependencies and Windows features |
| **--ignore-local-archive-malware-scan** | Ignore the malware scan performed as part of installing an archive-type package from a local manifest |
+| **--ignore-unavailable** | Ignore unavailable packages when installing multiple packages; remaining packages continue to install |
| **--dependency-source** | Find package dependencies using the specified source |
| **--accept-package-agreements** | Accept all license agreements for packages |
| **--no-upgrade** | Skip upgrade if an installed version already exists |
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");