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;