Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Dns
Dobbeleer
DONOT
dsc
dupenv
dustojnikhummer
dvinns
dwgs
Expand Down
1 change: 1 addition & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions src/AppInstallerCLICore/Commands/DscPackageResource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
48 changes: 48 additions & 0 deletions src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace AppInstallerCLIE2ETests
{
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
using AppInstallerCLIE2ETests.Helpers;
using NUnit.Framework;
Expand All @@ -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";
Expand Down Expand Up @@ -303,6 +305,52 @@ public void Package_Set_SimpleRepeated()
AssertDiffState(diff, []);
}

/// <summary>
/// Calls `set` on the `package` resource with `installMode` set to `silent`.
/// </summary>
[Test]
public void Package_Set_SilentInstallMode_UsesSilentAndCustomSwitches()
{
string installDir = Path.GetTempPath();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like optimistic πŸ€– here; the installDir is not a DSC property nor does it even attempt to pass it along. I think you would have to rely on the default install location or invent a new mechanism to pass it along in a side channel (like environment variable or known registry value). If you could update RunDSCv3Command to set an environment variable on the new process, that would be the safest way to not impact other tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried:

PS> @{id="Microsoft.Azd"; source="winget"; useLatest=$true; installMode="silent"} | ConvertTo-Json | wingetdev dscv3 package --set

and see:

2026-05-26 11:27:29.274 <I> [CLI ] Installer args: /passive /norestart /log "%LOCALAPPDATA%\Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir\Microsoft.Azd.1.25.300-26-05-26-11-27-29.log" INSTALLEDBY="winget"

Where https://github.com/microsoft/winget-pkgs/blob/9e3e07ee8ee60e7689f5f930a5f3852fe68fb03c/manifests/m/Microsoft/Azd/1.25.300/Microsoft.Azd.installer.yaml#L9 has the INSTALLEDBY="winget".

var environmentVariables = new Dictionary<string, string>();
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);
}

/// <summary>
/// Calls `set` on the `package` resource with `installMode` set to `interactive`.
/// </summary>
[Test]
public void Package_Set_InteractiveInstallMode_UsesInteractiveAndCustomSwitches()
{
string installDir = Path.GetTempPath();
var environmentVariables = new Dictionary<string, string>();
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);
}

/// <summary>
/// Calls `set` on the `package` resource to ensure that it is not present.
/// </summary>
Expand Down
11 changes: 9 additions & 2 deletions src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ public static void EnsureTestResourcePresence()
/// <param name="input">Input for the function; supports null, direct string, or JSON serialization of complex objects.</param>
/// <param name="timeOut">The maximum time to wait in milliseconds.</param>
/// <param name="throwOnTimeout">Whether to throw on a timeout or simply return the incomplete result.</param>
/// <param name="environmentVariables">Environment variables to set.</param>
/// <returns>A RunCommandResult containing the process exit code and output and error streams.</returns>
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<string, string> environmentVariables = null)
{
return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout);
return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout, environmentVariables);
}

/// <summary>
Expand Down
45 changes: 39 additions & 6 deletions src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace AppInstallerCLIE2ETests.Helpers
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
Expand Down Expand Up @@ -112,8 +113,15 @@ public static bool IsCIEnvironment
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <param name="environmentVariables">Environment variables to set.</param>
/// <returns>The result of the command.</returns>
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<string, string> environmentVariables = null)
{
string correlationParameter = " --correlation " + Guid.NewGuid().ToString();

Expand All @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -1159,15 +1167,25 @@ public static string CopyInstallerFileToARPInstallSourceDirectory(string install
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <param name="environmentVariables">Environment variables to set.</param>
/// <returns>The result of the command.</returns>
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<string, string> 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}");

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1251,10 +1277,17 @@ public static RunCommandResult RunProcess(string executablePath, string command,
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <param name="environmentVariables">Environment variables to set.</param>
/// <returns>The result of the command.</returns>
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<string, string> environmentVariables)
{
return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout);
return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout, environmentVariables);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLIE2ETests/InprocTestbedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
31 changes: 26 additions & 5 deletions src/AppInstallerTestExeInstaller/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <iostream>
#include <fstream>
#include <filesystem>
#include <optional>
#include <sstream>

using namespace std::filesystem;
Expand All @@ -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";
Expand Down Expand Up @@ -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<path> installDirectory;
std::wstringstream outContent;
std::wstring productCode;
std::wstring displayName;
Expand Down Expand Up @@ -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] << ' ';
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -650,16 +673,14 @@ 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.";
HandleRepairOperation(productCode, outContent, useHKLM);
}
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;
Expand Down
Loading