diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 5940a5f446..6551758cbd 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -150,6 +150,7 @@ Dns
Dobbeleer
DONOT
dsc
+dupenv
dustojnikhummer
dvinns
dwgs
diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md
index 60e8eda7bc..c9d54aadb8 100644
--- a/doc/ReleaseNotes.md
+++ b/doc/ReleaseNotes.md
@@ -86,3 +86,4 @@ Added a user setting (`logging.fileNameStrategy`) for controlling the default na
* DSC export now correctly exports WinGet Admin Settings
* `winget validate` now performs case-insensitive comparison for file extensions where applicable
* `winget source reset` now properly resets default sources instead of removing them
+* DSC v3 `Microsoft.WinGet/Package` resource now honors the `installMode` property to use silent or interactive installer switches as specified
diff --git a/src/AppInstallerCLICore/Commands/DscPackageResource.cpp b/src/AppInstallerCLICore/Commands/DscPackageResource.cpp
index 7671af5210..69f9633c1e 100644
--- a/src/AppInstallerCLICore/Commands/DscPackageResource.cpp
+++ b/src/AppInstallerCLICore/Commands/DscPackageResource.cpp
@@ -89,6 +89,20 @@ namespace AppInstaller::CLI
SubContext->Args.AddArg(Execution::Args::Type::AcceptSourceAgreements);
SubContext->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements);
}
+
+ std::string installMode = Utility::ToLower(Input.InstallMode().value_or("default"));
+ if (installMode == "silent")
+ {
+ SubContext->Args.AddArg(Execution::Args::Type::Silent);
+ }
+ else if (installMode == "interactive")
+ {
+ SubContext->Args.AddArg(Execution::Args::Type::Interactive);
+ }
+ else if (installMode != "default")
+ {
+ THROW_HR(E_INVALIDARG);
+ }
}
void PrepareSubContextInputs()
diff --git a/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs
index 3a938b19a8..d371defb5a 100644
--- a/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs
+++ b/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs
@@ -7,6 +7,7 @@
namespace AppInstallerCLIE2ETests
{
using System.Collections.Generic;
+ using System.IO;
using System.Text.Json.Serialization;
using AppInstallerCLIE2ETests.Helpers;
using NUnit.Framework;
@@ -22,6 +23,7 @@ public class DSCv3PackageResourceCommand : DSCv3ResourceTestBase
private const string DefaultPackageLowVersion = "1.0.0.0";
private const string DefaultPackageMidVersion = "1.1.0.0";
private const string DefaultPackageHighVersion = "2.0.0.0";
+ private const string DefaultPackageInstallLocationEnvironmentVariableName = "WINGET_TEST_EXE_INSTALL_LOCATION";
private const string PackageResource = "package";
private const string VersionPropertyName = "version";
private const string UseLatestPropertyName = "useLatest";
@@ -303,6 +305,52 @@ public void Package_Set_SimpleRepeated()
AssertDiffState(diff, []);
}
+ ///
+ /// Calls `set` on the `package` resource with `installMode` set to `silent`.
+ ///
+ [Test]
+ public void Package_Set_SilentInstallMode_UsesSilentAndCustomSwitches()
+ {
+ string installDir = Path.GetTempPath();
+ var environmentVariables = new Dictionary();
+ environmentVariables[DefaultPackageInstallLocationEnvironmentVariableName] = installDir;
+ PackageResourceData packageResourceData = new PackageResourceData()
+ {
+ Identifier = DefaultPackageIdentifier,
+ InstallMode = "silent",
+ };
+
+ var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData, environmentVariables: environmentVariables);
+ AssertSuccessfulResourceRun(ref result);
+
+ Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "/execustom"));
+ Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "/exesilent"));
+ TestCommon.BestEffortTestExeCleanup(installDir);
+ }
+
+ ///
+ /// Calls `set` on the `package` resource with `installMode` set to `interactive`.
+ ///
+ [Test]
+ public void Package_Set_InteractiveInstallMode_UsesInteractiveAndCustomSwitches()
+ {
+ string installDir = Path.GetTempPath();
+ var environmentVariables = new Dictionary();
+ environmentVariables[DefaultPackageInstallLocationEnvironmentVariableName] = installDir;
+ PackageResourceData packageResourceData = new PackageResourceData()
+ {
+ Identifier = DefaultPackageIdentifier,
+ InstallMode = "interactive",
+ };
+
+ var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData, environmentVariables: environmentVariables);
+ AssertSuccessfulResourceRun(ref result);
+
+ Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "/execustom"));
+ Assert.True(TestCommon.VerifyTestExeInstalled(installDir, "/exeinteractive"));
+ TestCommon.BestEffortTestExeCleanup(installDir);
+ }
+
///
/// Calls `set` on the `package` resource to ensure that it is not present.
///
diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs b/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs
index d3f3d0d789..fe3b9e2ac4 100644
--- a/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs
+++ b/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs
@@ -69,10 +69,17 @@ public static void EnsureTestResourcePresence()
/// Input for the function; supports null, direct string, or JSON serialization of complex objects.
/// The maximum time to wait in milliseconds.
/// Whether to throw on a timeout or simply return the incomplete result.
+ /// Environment variables to set.
/// A RunCommandResult containing the process exit code and output and error streams.
- protected static TestCommon.RunCommandResult RunDSCv3Command(string resource, string function, object input, int timeOut = 60000, bool throwOnTimeout = true)
+ protected static TestCommon.RunCommandResult RunDSCv3Command(
+ string resource,
+ string function,
+ object input,
+ int timeOut = 60000,
+ bool throwOnTimeout = true,
+ Dictionary environmentVariables = null)
{
- return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout);
+ return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout, environmentVariables);
}
///
diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
index 2b99865b68..6b3857fe39 100644
--- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
+++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
@@ -7,6 +7,7 @@
namespace AppInstallerCLIE2ETests.Helpers
{
using System;
+ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -112,8 +113,15 @@ public static bool IsCIEnvironment
/// Optional std in.
/// Optional timeout.
/// Throw on timeout.
+ /// Environment variables to set.
/// The result of the command.
- public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000, bool throwOnTimeout = true)
+ public static RunCommandResult RunAICLICommand(
+ string command,
+ string parameters,
+ string stdIn = null,
+ int timeOut = 60000,
+ bool throwOnTimeout = true,
+ Dictionary environmentVariables = null)
{
string correlationParameter = " --correlation " + Guid.NewGuid().ToString();
@@ -126,7 +134,7 @@ public static RunCommandResult RunAICLICommand(string command, string parameters
}
}
- return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout);
+ return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout, environmentVariables);
}
///
@@ -1159,15 +1167,25 @@ public static string CopyInstallerFileToARPInstallSourceDirectory(string install
/// Optional std in.
/// Optional timeout.
/// Throw on timeout.
+ /// Environment variables to set.
/// The result of the command.
- public static RunCommandResult RunProcess(string executablePath, string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
+ public static RunCommandResult RunProcess(
+ string executablePath,
+ string command,
+ string parameters,
+ string stdIn,
+ int timeOut,
+ bool throwOnTimeout,
+ Dictionary environmentVariables)
{
string inputMsg =
"Exe path: " + executablePath +
" Command: " + command +
" Parameters: " + parameters +
(string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) +
- " Timeout: " + timeOut;
+ " Timeout: " + timeOut +
+ (environmentVariables == null ? string.Empty :
+ " Env: " + string.Join(", ", environmentVariables.Select(item => $"{item.Key}={item.Value}")));
TestContext.Out.WriteLine($"Starting command run. {inputMsg}");
@@ -1203,6 +1221,14 @@ public static RunCommandResult RunProcess(string executablePath, string command,
p.StartInfo.RedirectStandardInput = true;
}
+ if (environmentVariables != null)
+ {
+ foreach (var item in environmentVariables)
+ {
+ p.StartInfo.EnvironmentVariables[item.Key] = item.Value;
+ }
+ }
+
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();
@@ -1251,10 +1277,17 @@ public static RunCommandResult RunProcess(string executablePath, string command,
/// Optional std in.
/// Optional timeout.
/// Throw on timeout.
+ /// Environment variables to set.
/// The result of the command.
- private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
+ private static RunCommandResult RunAICLICommandViaDirectProcess(
+ string command,
+ string parameters,
+ string stdIn,
+ int timeOut,
+ bool throwOnTimeout,
+ Dictionary environmentVariables)
{
- return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout);
+ return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout, environmentVariables);
}
///
diff --git a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs
index 29104e7bb1..229b6c2d37 100644
--- a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs
+++ b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs
@@ -212,7 +212,7 @@ private void RunInprocTestbed(TestbedParameters parameters, int timeout = 300000
builtParameters += $"-no-term ";
}
- var result = TestCommon.RunProcess(this.InprocTestbedPath, this.TargetPackageInformation, builtParameters, null, timeout, true);
+ var result = TestCommon.RunProcess(this.InprocTestbedPath, this.TargetPackageInformation, builtParameters, null, timeout, true, null);
Assert.AreEqual(0, result.ExitCode);
}
diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp
index cd93e3618f..b5b7bc9a11 100644
--- a/src/AppInstallerTestExeInstaller/main.cpp
+++ b/src/AppInstallerTestExeInstaller/main.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
using namespace std::filesystem;
@@ -17,6 +18,7 @@ std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}";
std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller";
std::wstring_view DefaultDisplayVersion = L"1.0.0.0";
std::wstring_view DscSubDirectoryName = L"SubDirectory";
+auto InstallLocationEnvironmentVariableName = "WINGET_TEST_EXE_INSTALL_LOCATION";
void WriteModifyRepairScript(std::wofstream& script, const path& repairCompletedTextFilePath, bool isModifyScript) {
std::wstring scriptName = isModifyScript ? L"Modify" : L"Uninstaller";
@@ -447,7 +449,7 @@ void HandleInstallationOperation(
// The installer prints all args to an output file and writes to the Uninstall registry key
int wmain(int argc, const wchar_t** argv)
{
- path installDirectory = temp_directory_path();
+ std::optional installDirectory;
std::wstringstream outContent;
std::wstring productCode;
std::wstring displayName;
@@ -476,7 +478,6 @@ int wmain(int argc, const wchar_t** argv)
if (++i < argc)
{
installDirectory = argv[i];
- std::filesystem::create_directories(installDirectory);
outContent << argv[i] << ' ';
}
}
@@ -616,6 +617,28 @@ int wmain(int argc, const wchar_t** argv)
}
}
+ if (!installDirectory)
+ {
+ char* value = nullptr;
+ size_t valueLength = 0;
+ errno_t result = _dupenv_s(&value, &valueLength, InstallLocationEnvironmentVariableName);
+ if (result == 0 && value)
+ {
+ installDirectory = value;
+ }
+ else
+ {
+ installDirectory = temp_directory_path();
+ }
+
+ if (value)
+ {
+ free(value);
+ }
+ }
+
+ std::filesystem::create_directories(installDirectory.value());
+
if (noOperation)
{
return exitCode;
@@ -650,8 +673,6 @@ int wmain(int argc, const wchar_t** argv)
displayVersion = DefaultDisplayVersion;
}
- path outFilePath = installDirectory;
-
if (isRepair)
{
outContent << L"\nInstaller Repair operation for AppInstallerTestExeInstaller.exe completed successfully.";
@@ -659,7 +680,7 @@ int wmain(int argc, const wchar_t** argv)
}
else
{
- HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion, noRepair, noModify, generateDscResourceFiles);
+ HandleInstallationOperation(*out, installDirectory.value(), outContent, productCode, useHKLM, displayName, displayVersion, noRepair, noModify, generateDscResourceFiles);
}
return exitCode;