diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp
index d56ecab462..17753d338c 100644
--- a/src/AppInstallerCLICore/Argument.cpp
+++ b/src/AppInstallerCLICore/Argument.cpp
@@ -92,7 +92,9 @@ namespace AppInstaller::CLI
case Execution::Args::Type::NoUpgrade:
return { type, "no-upgrade"_liv, ArgTypeCategory::CopyFlagToSubContext };
case Execution::Args::Type::SkipDependencies:
- return { type, "skip-dependencies"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext };
+ return { type, "skip-dependencies"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext, ArgTypeExclusiveSet::DependenciesConflict };
+ case Execution::Args::Type::DependenciesOnly:
+ return { type, "dependencies-only"_liv, ArgTypeCategory::InstallerBehavior, ArgTypeExclusiveSet::DependenciesConflict };
case Execution::Args::Type::AllowReboot:
return { type, "allow-reboot"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext };
@@ -408,6 +410,8 @@ namespace AppInstaller::CLI
return Argument{ type, Resource::String::HelpArgumentDescription, ArgumentType::Flag };
case Args::Type::SkipDependencies:
return Argument{ type, Resource::String::SkipDependenciesArgumentDescription, ArgumentType::Flag, false };
+ case Args::Type::DependenciesOnly:
+ 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::SourceName:
diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h
index 426568ba55..e7b438f1a7 100644
--- a/src/AppInstallerCLICore/Argument.h
+++ b/src/AppInstallerCLICore/Argument.h
@@ -91,6 +91,7 @@ namespace AppInstaller::CLI
AllAndTargetVersion = 0x40,
ConfigurationSetChoice = 0x80,
DscResourceFunction = 0x100,
+ DependenciesConflict = 0x200,
// This must always be at the end
Max
diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp
index 20eb99393a..15aeaec7ad 100644
--- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp
@@ -45,6 +45,7 @@ namespace AppInstaller::CLI
Argument::ForType(Args::Type::HashOverride),
Argument::ForType(Args::Type::AllowReboot),
Argument::ForType(Args::Type::SkipDependencies),
+ Argument::ForType(Args::Type::DependenciesOnly),
Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan),
Argument::ForType(Args::Type::DependencySource),
Argument::ForType(Args::Type::AcceptPackageAgreements),
@@ -150,6 +151,10 @@ namespace AppInstaller::CLI
{
flags = ProcessMultiplePackages::Flags::IgnoreDependencies;
}
+ else if (context.Args.Contains(Execution::Args::Type::DependenciesOnly))
+ {
+ flags = ProcessMultiplePackages::Flags::DependenciesOnly;
+ }
context <<
GetMultiSearchRequests <<
diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h
index eaa4eb775a..fa7e9c6358 100644
--- a/src/AppInstallerCLICore/ExecutionArgs.h
+++ b/src/AppInstallerCLICore/ExecutionArgs.h
@@ -45,6 +45,7 @@ namespace AppInstaller::CLI::Execution
InstallerType,
HashOverride, // Ignore hash mismatches
SkipDependencies, // Skip dependencies
+ DependenciesOnly, // Install only dependencies, not the target package
IgnoreLocalArchiveMalwareScan, // Ignore the local malware scan on archive files
AcceptPackageAgreements, // Accept all license agreements for packages
Rename, // Renames the file of the executable. Only applies to the portable installerType
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index 3fa69a1a0c..ef3db8b395 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -686,6 +686,8 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SingleCharAfterDashError);
WINGET_DEFINE_RESOURCE_STRINGID(SkipDependenciesArgumentDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(DependenciesOnlyArgumentDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(DependenciesOnlyMessage);
WINGET_DEFINE_RESOURCE_STRINGID(SkipMicrosoftStorePackageLicenseArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentArg);
WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentName);
diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
index 130cddb2b2..f702629c20 100644
--- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
@@ -157,6 +157,16 @@ namespace AppInstaller::CLI::Workflow
HRESULT HResult;
Resource::StringId Message;
};
+
+ void CheckForOnlyDependencies(Execution::Context& context)
+ {
+ if (context.Args.Contains(Execution::Args::Type::DependenciesOnly))
+ {
+ context.Reporter.Info() << Resource::String::DependenciesOnlyMessage << std::endl;
+ // We want the context to terminate, but successfully.
+ context.SetTerminationHR(S_OK);
+ }
+ }
}
namespace details
@@ -640,6 +650,7 @@ namespace AppInstaller::CLI::Workflow
Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) <<
Workflow::CreateDependencySubContexts(Resource::String::PackageRequiresDependencies) <<
Workflow::InstallDependencies <<
+ CheckForOnlyDependencies <<
Workflow::DownloadInstaller <<
Workflow::InstallPackageInstaller <<
Workflow::RegisterStartupAfterReboot();
@@ -712,6 +723,7 @@ namespace AppInstaller::CLI::Workflow
m_stopOnFailure = WI_IsFlagSet(flags, Flags::StopOnFailure);
m_refreshPathVariable = WI_IsFlagSet(flags, Flags::RefreshPathVariable);
m_downloadOnly = WI_IsFlagSet(flags, Flags::DownloadOnly);
+ m_dependenciesOnly = WI_IsFlagSet(flags, Flags::DependenciesOnly);
}
void ProcessMultiplePackages::operator()(Execution::Context& context) const
@@ -763,6 +775,11 @@ namespace AppInstaller::CLI::Workflow
size_t packagesCount = packageSubContexts.size();
size_t packagesProgress = 0;
+ if (m_dependenciesOnly)
+ {
+ context.Reporter.Info() << Resource::String::DependenciesOnlyMessage << std::endl;
+ }
+
for (auto& packageContext : packageSubContexts)
{
packagesProgress++;
@@ -786,11 +803,14 @@ namespace AppInstaller::CLI::Workflow
Workflow::ProcessMultiplePackages(m_dependenciesReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, Flags::IgnoreDependencies | Flags::StopOnFailure | Flags::RefreshPathVariable);
}
- currentContext << Workflow::DownloadInstaller;
-
- if (!downloadInstallerOnly)
+ if (!m_dependenciesOnly)
{
- currentContext << Workflow::InstallPackageInstaller;
+ currentContext << Workflow::DownloadInstaller;
+
+ if (!downloadInstallerOnly)
+ {
+ currentContext << Workflow::InstallPackageInstaller;
+ }
}
}
catch (...)
diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h
index b80c5d6654..2f2c056ab8 100644
--- a/src/AppInstallerCLICore/Workflows/InstallFlow.h
+++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h
@@ -182,6 +182,7 @@ namespace AppInstaller::CLI::Workflow
StopOnFailure = 0x04,
RefreshPathVariable = 0x08,
DownloadOnly = 0x10,
+ DependenciesOnly = 0x20,
};
ProcessMultiplePackages(
@@ -201,6 +202,7 @@ namespace AppInstaller::CLI::Workflow
bool m_stopOnFailure;
bool m_refreshPathVariable;
bool m_downloadOnly;
+ bool m_dependenciesOnly;
};
DEFINE_ENUM_FLAG_OPERATORS(ProcessMultiplePackages::Flags);
diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs
index e5de1868e1..53921ec817 100644
--- a/src/AppInstallerCLIE2ETests/InstallCommand.cs
+++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs
@@ -709,6 +709,30 @@ public void InstallWithPackageDependency_RefreshPathVariable()
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(testDir));
}
+ ///
+ /// Test install a package with a package dependency and specify dependencies only.
+ ///
+ [Test]
+ public void InstallWithPackageDependency_DependenciesOnly()
+ {
+ var testDir = TestCommon.GetRandomTestDir();
+ string installDir = TestCommon.GetPortablePackagesDirectory();
+ var installResult = TestCommon.RunAICLICommand("install", $"-q AppInstallerTest.PackageDependencyRequiresPathRefresh -q AppInstallerTest.TestExeInstaller -l {testDir} --dependencies-only");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode);
+ Assert.True(installResult.StdOut.Contains("Installing dependencies only. The package itself will not be installed."));
+ Assert.True(installResult.StdOut.Contains("Successfully installed"));
+
+ // Portable package is used as a dependency. Ensure that it is installed and cleaned up successfully.
+ string portablePackageId, commandAlias, fileName, packageDirName, productCode;
+ portablePackageId = "AppInstallerTest.TestPortableExeWithCommand";
+ packageDirName = productCode = portablePackageId + "_" + Constants.TestSourceIdentifier;
+ fileName = "AppInstallerTestExeInstaller.exe";
+ commandAlias = "testCommand.exe";
+
+ TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true);
+ Assert.False(TestCommon.VerifyTestExeInstalledAndCleanup(testDir));
+ }
+
///
/// Test install a package using a specific installer type.
///
diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index 7ee024e54d..6cfee598b7 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -2024,6 +2024,12 @@ Please specify one of them using the --source option to proceed.
Skips processing package dependencies and Windows features
+
+ Installs only the dependencies of the package
+
+
+ Installing dependencies only. The package itself will not be installed.
+
Dependencies skipped.
diff --git a/src/AppInstallerCLITests/InstallDependenciesFlow.cpp b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp
index c3f81912de..d566bb649d 100644
--- a/src/AppInstallerCLITests/InstallDependenciesFlow.cpp
+++ b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp
@@ -232,6 +232,28 @@ TEST_CASE("InstallerWithDependencies_IgnoreDependenciesSetting", "[dependencies]
REQUIRE_FALSE(installOutput.str().find("PreviewIIS") != std::string::npos);
}
+TEST_CASE("InstallerWithDependencies_DependenciesOnly", "[dependencies]")
+{
+ std::ostringstream installOutput;
+ TestContext context{ installOutput, std::cin };
+ auto previousThreadGlobals = context.SetForCurrentThread();
+ OverrideOpenDependencySource(context);
+ OverrideEnableWindowsFeaturesDependencies(context);
+
+ context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string());
+ context.Args.AddArg(Execution::Args::Type::DependenciesOnly);
+
+ InstallCommand install({});
+ install.Execute(context);
+ INFO(installOutput.str());
+
+ // Dependencies should be reported and installed
+ REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageRequiresDependencies).get()) != std::string::npos);
+ REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos);
+ // DependenciesOnly message should be shown
+ REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesOnlyMessage).get()) != std::string::npos);
+}
+
TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]")
{
std::ostringstream installOutput;