winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 40d9cd3bc7839e1c3fe78b4bfccf32fd6001c9bd
parent edf4fbe68061b1a3a60f436f7549e5ef230dfd6a
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Wed, 10 May 2023 11:34:32 -0700

Allow ${WinGetConfigRoot} variable expansion (#3237)

Allow settings to use the `$WinGetConfigRoot` variable.

This variable means the directory containing the configuration file and is helpful for units that want reference paths that are relative to the location of the configuration file.

For example, our test resource can use it like this:
```
properties:
  configurationVersion: 0.2
  resources:
    - resource: xSimpleTestResource/SimpleFileResource
      settings:
        path: '${WinGetConfigRoot}\..\test.txt'
        content: 'Ella baila sola'
```

To make it PowerShelly, the variable expansion is done ignoring case.

Since this is still in preview, the name of the variable can change when `winget configure` release.
Diffstat:
M.github/actions/spelling/expect.txt | 1+
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 1+
Msrc/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs | 5+++++
Asrc/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs | 39+++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs | 8+++-----
Msrc/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs | 4++--
Msrc/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs | 6+++---
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs | 2+-
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs | 2+-
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs | 1+
12 files changed, 186 insertions(+), 14 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -431,6 +431,7 @@ winapifamily windir windowsdeveloper winerror +wingetconfigroot wingetcreate wingetdev wingetutil diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -178,6 +178,7 @@ #define WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT ((HRESULT)0x8A15C107) #define WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE ((HRESULT)0x8A15C108) #define WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT ((HRESULT)0x8A15C109) +#define WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT ((HRESULT)0x8A15C110) namespace AppInstaller { diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -60,5 +60,10 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions /// The unit returned an invalid result. /// </summary> internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); + + /// <summary> + /// The unit contains a setting that requires config root. + /// </summary> + internal const int WinGetConfigUnitSettingConfigRoot = unchecked((int)0x8A15C110); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="UnitSettingConfigRootException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// <summary> + /// A setting uses the config root variable and the Path was not set in the ConfigurationSet. + /// </summary> + internal class UnitSettingConfigRootException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="UnitSettingConfigRootException"/> class. + /// </summary> + /// <param name="unitName">Unit name.</param> + /// <param name="setting">Setting.</param> + public UnitSettingConfigRootException(string unitName, string setting) + : base($"Unit: {unitName} Setting {setting} requires the ConfigurationSet Path") + { + this.HResult = ErrorCodes.WinGetConfigUnitSettingConfigRoot; + this.UnitName = unitName; + this.Setting = setting; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string UnitName { get; } + + /// <summary> + /// Gets the setting that reference the config root variable. + /// </summary> + public string Setting { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs @@ -99,14 +99,12 @@ namespace Microsoft.Management.Configuration.Processor.Helpers } /// <summary> - /// TODO: Implement. - /// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted. - /// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that. + /// Gets the settings of the unit. /// </summary> /// <returns>ValueSet with settings.</returns> - public ValueSet GetExpandedSettings() + public ValueSet GetSettings() { - return this.Unit.Settings; + return this.UnitInternal.GetExpandedSettings(); } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs @@ -6,9 +6,13 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { + using System; using System.Collections.Generic; + using System.IO; using Microsoft.Management.Configuration.Processor.Constants; + using Microsoft.Management.Configuration.Processor.Exceptions; using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; /// <summary> /// Wrapper around Configuration units and its directives. Creates a normalized directives map @@ -16,15 +20,20 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// </summary> internal class ConfigurationUnitInternal { + private const string ConfigRootVar = "${WinGetConfigRoot}"; + + private readonly string? configurationFileRootPath = null; private readonly Dictionary<string, object> normalizedDirectives = new (); /// <summary> /// Initializes a new instance of the <see cref="ConfigurationUnitInternal"/> class. /// </summary> /// <param name="unit">Configuration unit.</param> + /// <param name="configurationFilePath">The configuration file path.</param> /// <param name="directivesOverlay">Directives overlay.</param> public ConfigurationUnitInternal( ConfigurationUnit unit, + string configurationFilePath, IReadOnlyDictionary<string, object>? directivesOverlay = null) { this.Unit = unit; @@ -45,6 +54,16 @@ namespace Microsoft.Management.Configuration.Processor.Helpers this.GetDirective<string>(DirectiveConstants.MaxVersion), this.GetDirective<string>(DirectiveConstants.ModuleGuid)); } + + if (!string.IsNullOrEmpty(configurationFilePath)) + { + if (!File.Exists(configurationFilePath)) + { + throw new FileNotFoundException(configurationFilePath); + } + + this.configurationFileRootPath = Path.GetDirectoryName(configurationFilePath); + } } /// <summary> @@ -150,6 +169,57 @@ namespace Microsoft.Management.Configuration.Processor.Helpers return null; } + /// <summary> + /// TODO: Implement for more variables. + /// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted. + /// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that. + /// </summary> + /// <returns>ValueSet with settings.</returns> + public ValueSet GetExpandedSettings() + { + var valueSet = new ValueSet(); + foreach (var value in this.Unit.Settings) + { + if (value.Value is string) + { + // For now, we just expand config root. + valueSet.Add(value.Key, this.ExpandConfigRoot(value.Value as string, value.Key)); + } + else + { + valueSet.Add(value); + } + } + + return valueSet; + } + + private string? ExpandConfigRoot(string? value, string settingName) + { + if (!string.IsNullOrEmpty(value)) + { + // TODO: since we only support one variable, this only finds and replace + // ${WingetConfigRoot} if found in the string when the work of expanding + // string is done it should take into account other operators like the subexpression operator $() + if (value.Contains(ConfigRootVar, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(this.configurationFileRootPath)) + { + throw new UnitSettingConfigRootException(this.Unit.UnitName, settingName); + } + + if (this.configurationFileRootPath == null) + { + throw new ArgumentException(); + } + + return value.Replace(ConfigRootVar, this.configurationFileRootPath, StringComparison.OrdinalIgnoreCase); + } + } + + return value; + } + private void InitializeDirectives() { // Overlay directives have precedence. diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -57,7 +57,7 @@ namespace Microsoft.Management.Configuration.Processor.Set { try { - var configurationUnitInternal = new ConfigurationUnitInternal(unit, directivesOverlay); + var configurationUnitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path, directivesOverlay); this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.ToIdentifyingString()}..."); var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); @@ -87,7 +87,7 @@ namespace Microsoft.Management.Configuration.Processor.Set { try { - var unitInternal = new ConfigurationUnitInternal(unit); + var unitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path); this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailLevel}] for: {unitInternal.ToIdentifyingString()}"); var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs @@ -64,7 +64,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit try { result.Settings = this.processorEnvironment.InvokeGetResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); } @@ -97,7 +97,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit try { bool testResult = this.processorEnvironment.InvokeTestResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); @@ -132,7 +132,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit try { result.RebootRequired = this.processorEnvironment.InvokeSetResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs @@ -189,7 +189,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // This is easier than trying to mock sealed class from external code... var testEnv = this.fixture.PrepareTestProcessorEnvironment(true); - var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null)); var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); if (dscResourceInfo is null || psModuleInfo is null) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs @@ -694,7 +694,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { // This is easier than trying to mock sealed class from external code... var testEnv = this.fixture.PrepareTestProcessorEnvironment(true); - var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null)); var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); if (dscResourceInfo is null || psModuleInfo is null) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs @@ -8,10 +8,13 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using System; using System.Collections.Generic; + using System.IO; using System.Management.Automation; using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Exceptions; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; using Xunit; using Xunit.Abstractions; @@ -73,7 +76,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { anotherDirective, overlayAnother }, }; - var unitInternal = new ConfigurationUnitInternal(unit, overlays); + var unitInternal = new ConfigurationUnitInternal(unit, string.Empty, overlays); var description = unitInternal.GetDirective<string>(descriptionDirective); Assert.Equal(description, overlayDescription); @@ -107,7 +110,61 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests unit.Directives.Add("version", "not a version"); Assert.Throws<ArgumentException>( - () => new ConfigurationUnitInternal(unit, null)); + () => new ConfigurationUnitInternal(unit, string.Empty, null)); + } + + /// <summary> + /// Verifies expansion of ConfigRoot. + /// </summary> + [Fact] + public void GetExpandedSettings_ConfigRoot() + { + using var tmpFile = new TempFile("fakeConfigFile.yml", content: "content"); + + var unit = new ConfigurationUnit(); + unit.Settings.Add("var1", @"$WinGetConfigRoot\this\is\a\path.txt"); + unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt"); + unit.Settings.Add("var3", @"this\is\a\$WINGETCONFIGROOT\path.txt"); + unit.Settings.Add("var4", @"this\is\a\${WINGETCONFIGROOT}\path.txt"); + unit.Settings.Add("var5", @"this\is\a\path\$wingetconfigroot"); + unit.Settings.Add("var6", @"this\is\a\path\${wingetconfigroot}"); + + string configPath = tmpFile.FullFileName; + string? expectedPath = Path.GetDirectoryName(configPath); + var unitInternal = new ConfigurationUnitInternal(unit, configPath); + + var expandedSettings = unitInternal.GetExpandedSettings(); + + var var1 = expandedSettings["var1"]; + Assert.Equal(@"$WinGetConfigRoot\this\is\a\path.txt", var1 as string); + + var var2 = expandedSettings["var2"]; + Assert.Equal($@"{expectedPath}\this\is\a\path.txt", var2 as string); + + var var3 = expandedSettings["var3"]; + Assert.Equal(@"this\is\a\$WINGETCONFIGROOT\path.txt", var3 as string); + + var var4 = expandedSettings["var4"]; + Assert.Equal($@"this\is\a\{expectedPath}\path.txt", var4 as string); + + var var5 = expandedSettings["var5"]; + Assert.Equal(@"this\is\a\path\$wingetconfigroot", var5 as string); + + var var6 = expandedSettings["var6"]; + Assert.Equal($@"this\is\a\path\{expectedPath}", var6 as string); + } + + /// <summary> + /// Verifies throws when config root is not set. + /// </summary> + [Fact] + public void GetExpandedSetting_ConfigRoot_Throw() + { + var unit = new ConfigurationUnit(); + unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt"); + + var unitInternal = new ConfigurationUnitInternal(unit, null!); + Assert.Throws<UnitSettingConfigRootException>(() => unitInternal.GetExpandedSettings()); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs @@ -362,6 +362,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests UnitName = resourceName, Intent = intent, }, + string.Empty, new Dictionary<string, object>()), new DscResourceInfoInternal(resourceName, null, null)); }