From d5c58c97a7a30d6c7b102e2fad4bbcd5af86b9fa Mon Sep 17 00:00:00 2001 From: Ryan <69221034+ryfu-msft@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:40:30 -0500 Subject: [PATCH] Add default module root setting for configuration (#4974) --- doc/Settings.md | 15 +++++++++++ .../JSON/settings/settings.schema.0.2.json | 11 ++++++++ .../ConfigurationCommon.cpp | 11 ++++++-- .../ConfigureCommand.cs | 25 +++++++++++++++++++ src/AppInstallerCLIE2ETests/Constants.cs | 1 + src/AppInstallerCLIE2ETests/ErrorCommand.cs | 13 ++++++++-- .../Helpers/WinGetSettingsHelper.cs | 20 +++++++++++++++ src/AppInstallerCLITests/UserSettings.cpp | 15 +++++++++++ .../Public/winget/UserSettings.h | 4 +++ src/AppInstallerCommonCore/UserSettings.cpp | 5 ++++ 10 files changed, 116 insertions(+), 4 deletions(-) diff --git a/doc/Settings.md b/doc/Settings.md index f593fc946f..027c0066ab 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -200,6 +200,21 @@ The `purgePortablePackage` behavior affects the default behavior for uninstallin }, ``` +## Configure Behavior + +The `configureBehavior` settings affect the default behavior of applying a configuration. + +### Default Module Root +The `defaultModuleRoot` behavior affects the default root directory where modules are installed to. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Configuration/Modules` if value is not set or is invalid. + +> Note: This setting value must be an absolute path. + +```json + "configureBehavior": { + "defaultModuleRoot": "C:/Program Files/Modules/" + }, +``` + ## Telemetry The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 2113127f0e..099ebe8080 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -198,6 +198,17 @@ } } }, + "ConfigureBehavior": { + "description": "Configure settings", + "type": "object", + "properties": { + "defaultModuleRoot": { + "description": "The default root directory where PowerShell modules are installed to when applying a configuration.", + "type": "string", + "maxLength": 32767 + } + } + }, "DownloadBehavior": { "description": "Download settings", "type": "object", diff --git a/src/AppInstallerCLICore/ConfigurationCommon.cpp b/src/AppInstallerCLICore/ConfigurationCommon.cpp index c0ba7ef164..7111757493 100644 --- a/src/AppInstallerCLICore/ConfigurationCommon.cpp +++ b/src/AppInstallerCLICore/ConfigurationCommon.cpp @@ -21,7 +21,7 @@ namespace AppInstaller::CLI struct ModulePathInfo { SetProcessorFactory::PwshConfigurationProcessorLocation location; - std::optional customLocation; + std::optional customLocation; }; ModulePathInfo GetModulePathInfo(Execution::Args& execArgs) @@ -44,10 +44,17 @@ namespace AppInstaller::CLI } else { - return { SetProcessorFactory::PwshConfigurationProcessorLocation::Custom, execArgs.GetArg(Execution::Args::Type::ConfigurationModulePath) }; + return { SetProcessorFactory::PwshConfigurationProcessorLocation::Custom, std::string(execArgs.GetArg(Execution::Args::Type::ConfigurationModulePath)) }; } } + std::filesystem::path defaultModuleRoot = Settings::User().Get(); + + if (!defaultModuleRoot.empty()) + { + return { SetProcessorFactory::PwshConfigurationProcessorLocation::Custom, defaultModuleRoot.u8string() }; + } + return { SetProcessorFactory::PwshConfigurationProcessorLocation::WinGetModulePath, {} }; } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index f296b68df0..ae8a2b0a90 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -76,6 +76,31 @@ public void ConfigureFromTestRepo() Constants.SimpleTestModuleName))); } + /// + /// Simple test to confirm that the module was installed to the location specified in the DefaultModuleRoot settings. + /// + [Test] + public void ConfigureFromTestRepo_DefaultModuleRootSetting() + { + TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); + string moduleTestDir = TestCommon.GetRandomTestDir(); + WinGetSettingsHelper.ConfigureConfigureBehavior(Constants.DefaultModuleRoot, moduleTestDir); + + string args = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo_Location.yml"); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, args); + + WinGetSettingsHelper.ConfigureConfigureBehavior(Constants.DefaultModuleRoot, string.Empty); + bool moduleExists = Directory.Exists(Path.Combine(moduleTestDir, Constants.SimpleTestModuleName)); + if (moduleExists) + { + // Clean test directory to avoid impacting other tests. + Directory.Delete(moduleTestDir, true); + } + + Assert.AreEqual(0, result.ExitCode); + Assert.True(moduleExists); + } + /// /// Simple test to confirm that the module was installed in the right location. /// diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 25fcb6d72c..70d3023a18 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -125,6 +125,7 @@ public class Constants public const string PortablePackageMachineRoot = "portablePackageMachineRoot"; public const string InstallBehaviorScope = "scope"; public const string InstallerTypes = "installerTypes"; + public const string DefaultModuleRoot = "defaultModuleRoot"; // Configuration public const string PSGalleryName = "PSGallery"; diff --git a/src/AppInstallerCLIE2ETests/ErrorCommand.cs b/src/AppInstallerCLIE2ETests/ErrorCommand.cs index 8bc67d6b76..10a257defe 100644 --- a/src/AppInstallerCLIE2ETests/ErrorCommand.cs +++ b/src/AppInstallerCLIE2ETests/ErrorCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -14,6 +14,15 @@ namespace AppInstallerCLIE2ETests /// public class ErrorCommand { + /// + /// Reset settings file to avoid affecting output from error command. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.InitializeWingetSettings(); + } + /// /// Tests 0. /// @@ -127,4 +136,4 @@ public void String() Assert.True(result.StdOut.Contains("APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE")); } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index 0a5e2cf069..703d177b5e 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -73,6 +73,12 @@ public static void InitializeWingetSettings() { } }, + { + "configureBehavior", + new Hashtable() + { + } + }, }; // Run winget one time to initialize settings directory @@ -108,6 +114,20 @@ public static void ConfigureInstallBehavior(string settingName, string value) SetWingetSettings(settingsJson); } + /// + /// Configure the configuration behavior. + /// + /// Setting name. + /// Setting value. + public static void ConfigureConfigureBehavior(string settingName, string value) + { + JObject settingsJson = GetJsonSettingsObject("configureBehavior"); + var configureBehavior = settingsJson["configureBehavior"]; + configureBehavior[settingName] = value; + + SetWingetSettings(settingsJson); + } + /// /// Configure the install behavior preferences. /// diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index ed54c36bbc..3d0820f8d5 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -526,6 +526,21 @@ TEST_CASE("SettingsDownloadDefaultDirectory", "[settings]") } } +TEST_CASE("SettingsConfigureDefaultModuleRoot", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Valid path") + { + std::string_view json = R"({ "configureBehavior": { "defaultModuleRoot": "C:/Foo/Bar" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == "C:/Foo/Bar"); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } +} + TEST_CASE("SettingsArchiveExtractionMethod", "[settings]") { auto again = DeleteUserSettingsFiles(); diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index b580775c28..d13d3e8f46 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -108,6 +108,8 @@ namespace AppInstaller::Settings UninstallPurgePortablePackage, // Download behavior DownloadDefaultDirectory, + // Configure behavior + ConfigureDefaultModuleRoot, // Interactivity InteractivityDisable, #ifndef AICLI_DISABLE_TEST_HOOKS @@ -185,6 +187,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::UninstallPurgePortablePackage, bool, bool, false, ".uninstallBehavior.purgePortablePackage"sv); // Download behavior SETTINGMAPPING_SPECIALIZATION(Setting::DownloadDefaultDirectory, std::string, std::filesystem::path, {}, ".downloadBehavior.defaultDownloadDirectory"sv); + // Configure behavior + SETTINGMAPPING_SPECIALIZATION(Setting::ConfigureDefaultModuleRoot, std::string, std::filesystem::path, {}, ".configureBehavior.defaultModuleRoot"sv); // Network SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDownloader, std::string, InstallerDownloader, InstallerDownloader::Default, ".network.downloader"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 9e13610537..d6879e9b9a 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -401,6 +401,11 @@ namespace AppInstaller::Settings return ValidatePathValue(value); } + WINGET_VALIDATE_SIGNATURE(ConfigureDefaultModuleRoot) + { + return ValidatePathValue(value); + } + WINGET_VALIDATE_SIGNATURE(NetworkDownloader) { static constexpr std::string_view s_downloader_default = "default";