Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Argument.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ namespace AppInstaller::CLI
AllAndTargetVersion = 0x40,
ConfigurationSetChoice = 0x80,
DscResourceFunction = 0x100,
DependenciesConflict = 0x200,

// This must always be at the end
Max
Expand Down
5 changes: 5 additions & 0 deletions src/AppInstallerCLICore/Commands/InstallCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 <<
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/ExecutionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 24 additions & 4 deletions src/AppInstallerCLICore/Workflows/InstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand All @@ -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 (...)
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Workflows/InstallFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ namespace AppInstaller::CLI::Workflow
StopOnFailure = 0x04,
RefreshPathVariable = 0x08,
DownloadOnly = 0x10,
DependenciesOnly = 0x20,
};

ProcessMultiplePackages(
Expand All @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/AppInstallerCLIE2ETests/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,30 @@ public void InstallWithPackageDependency_RefreshPathVariable()
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(testDir));
}

/// <summary>
/// Test install a package with a package dependency and specify dependencies only.
/// </summary>
[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));
}

/// <summary>
/// Test install a package using a specific installer type.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2024,6 +2024,12 @@ Please specify one of them using the --source option to proceed.</value>
<data name="SkipDependenciesArgumentDescription" xml:space="preserve">
<value>Skips processing package dependencies and Windows features</value>
</data>
<data name="DependenciesOnlyArgumentDescription" xml:space="preserve">
<value>Installs only the dependencies of the package</value>
</data>
<data name="DependenciesOnlyMessage" xml:space="preserve">
<value>Installing dependencies only. The package itself will not be installed.</value>
</data>
<data name="DependenciesSkippedMessage" xml:space="preserve">
<value>Dependencies skipped.</value>
</data>
Expand Down
22 changes: 22 additions & 0 deletions src/AppInstallerCLITests/InstallDependenciesFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading