diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 61a4012d6..6148fbb3d 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -185,15 +185,17 @@ namespace Microsoft.PowerShell { profileDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\Windows\PowerShell"; - } else + + if (!Directory.Exists(profileDir)) + { + Directory.CreateDirectory(profileDir); + } + } + else { - profileDir = System.IO.Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".powershell"); + profileDir = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE); } - if (!Directory.Exists(profileDir)) - { - Directory.CreateDirectory(profileDir); - } ClrFacade.SetProfileOptimizationRoot(profileDir); } catch @@ -268,7 +270,6 @@ namespace Microsoft.PowerShell : "StartupProfileData-NonInteractive"); exitCode = theConsoleHost.Run(cpp, !string.IsNullOrEmpty(preStartWarning)); } - } finally { diff --git a/src/Microsoft.PowerShell.PSReadLine/Cmdlets.cs b/src/Microsoft.PowerShell.PSReadLine/Cmdlets.cs index 3210a8965..8c2f37004 100644 --- a/src/Microsoft.PowerShell.PSReadLine/Cmdlets.cs +++ b/src/Microsoft.PowerShell.PSReadLine/Cmdlets.cs @@ -169,24 +169,39 @@ namespace Microsoft.PowerShell WordDelimiters = DefaultWordDelimiters; HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive; HistorySaveStyle = DefaultHistorySaveStyle; + + string historyFileName = hostName + "_history.txt"; #if CORECLR if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) // MS Windows { - HistorySavePath = System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%AppData%"), - @"\Microsoft\Windows\PowerShell\PSReadline\", - hostName + "_history.txt"); + HistorySavePath = System.IO.Path.Combine(Environment.GetEnvironmentVariable("APPDATA"), + @"Microsoft\Windows\PowerShell\PSReadline\", + historyFileName); } else { - HistorySavePath = System.IO.Path.Combine( - Environment.GetEnvironmentVariable("HOME"), - ".powershell", - "PSReadLine", - hostName + "_history.txt"); + // PSReadline does not have access to Utils.CorePSPlatform. Must set PSReadline path separately + string historyPath = System.Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + + if (!String.IsNullOrEmpty(historyPath)) + { + historyPath = System.IO.Path.Combine(historyPath, "powershell", "PSReadLine", historyFileName); + HistorySavePath = historyPath; + } + else + { + // History is data, so it goes into .local/share/powershell folder + HistorySavePath = System.IO.Path.Combine(Environment.GetEnvironmentVariable("HOME"), + ".local", + "share", + "powershell", + "PSReadLine", + historyFileName); + } } #else HistorySavePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) - + @"\Microsoft\Windows\PowerShell\PSReadline\" + hostName + "_history.txt"; + + @"\Microsoft\Windows\PowerShell\PSReadline\" + historyFileName; #endif CommandValidationHandler = null; CommandsToValidateScriptBlockArguments = new HashSet(StringComparer.OrdinalIgnoreCase) diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs index 34623fc1b..1110b2623 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs @@ -44,6 +44,15 @@ namespace System.Management.Automation } } + //enum for selecting the xdgpaths + public enum XDG_Type + { + PROFILE, + MODULES, + CACHE, + DEFAULT + } + public static bool IsOSX { get @@ -96,8 +105,94 @@ namespace System.Management.Automation "WSMan.format.ps1xml" }; - // directory location of PowerShell for profile loading - public static string ProductNameForDirectory = ".powershell"; + // function for choosing directory location of PowerShell for profile loading + public static string SelectProductNameForDirectory (Platform.XDG_Type dirpath) + { + + //TODO: XDG_DATA_DIRS implementation as per GitHub issue #1060 + + string xdgconfighome = System.Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + string xdgdatahome = System.Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + string xdgcachehome = System.Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + string xdgConfigHomeDefault = Path.Combine ( System.Environment.GetEnvironmentVariable("HOME"), ".config", "powershell"); + string xdgModuleDefault = Path.Combine ( System.Environment.GetEnvironmentVariable("HOME"), ".local", "share", "powershell", "Modules"); + string xdgCacheDefault = Path.Combine (System.Environment.GetEnvironmentVariable("HOME"), ".cache", "powershell"); + + switch (dirpath){ + case Platform.XDG_Type.PROFILE: + //the user has set XDG_CONFIG_HOME corrresponding to profile path + if (String.IsNullOrEmpty(xdgconfighome)) + { + //xdg values have not been set + return xdgConfigHomeDefault; + } + + else + { + return Path.Combine(xdgconfighome, "powershell"); + } + + case Platform.XDG_Type.MODULES: + //the user has set XDG_DATA_HOME corresponding to module path + if (String.IsNullOrEmpty(xdgdatahome)){ + + //xdg values have not been set + if (!Directory.Exists(xdgModuleDefault)) //module folder not always guaranteed to exist + { + Directory.CreateDirectory(xdgModuleDefault); + } + return xdgModuleDefault; + } + else + { + return Path.Combine(xdgdatahome, "powershell", "Modules"); + } + + case Platform.XDG_Type.CACHE: + //the user has set XDG_CACHE_HOME + if (String.IsNullOrEmpty(xdgcachehome)) + { + //xdg values have not been set + if (!Directory.Exists(xdgCacheDefault)) //module folder not always guaranteed to exist + { + Directory.CreateDirectory(xdgCacheDefault); + } + + return xdgCacheDefault; + } + + else + { + if (!Directory.Exists(Path.Combine(xdgcachehome, "powershell"))) + { + Directory.CreateDirectory(Path.Combine(xdgcachehome, "powershell")); + } + + return Path.Combine(xdgcachehome, "powershell"); + } + + case Platform.XDG_Type.DEFAULT: + //default for profile location + return xdgConfigHomeDefault; + + default: + //xdgConfigHomeDefault needs to be created in the edge case that we do not have the folder or it was deleted + //This folder is the default in the event of all other failures for data storage + if (!Directory.Exists(xdgConfigHomeDefault)) + { + try { + Directory.CreateDirectory(xdgConfigHomeDefault); + } + catch{ + + Console.Error.WriteLine("Failed to create default data directory: " + xdgConfigHomeDefault); + } + } + + return xdgConfigHomeDefault; + } + + } // ComObjectType is null on CoreCLR for Linux since there is // no COM support on Linux @@ -363,7 +458,7 @@ namespace System.Management.Automation throw new InvalidOperationException("LinuxPlatform.NonWindowsHostName error: " + lastError); } return hostName; - + } else { @@ -386,7 +481,7 @@ namespace System.Management.Automation // TODO:PSL clean this up return 0; } - + /// /// This exception is meant to be thrown if a code path is not supported due /// to platform restrictions @@ -570,7 +665,7 @@ namespace System.Management.Automation int ret = Native.IsSymLink(filePath); switch(ret) { - case 1: + case 1: return true; case 0: return false; @@ -590,7 +685,7 @@ namespace System.Management.Automation int ret = Native.IsExecutable(filePath); switch(ret) { - case 1: + case 1: return true; case 0: return false; @@ -685,11 +780,11 @@ namespace System.Management.Automation internal static extern int SetDate(SetDateInfoInternal info); [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] - internal static extern int CreateSymLink([MarshalAs(UnmanagedType.LPStr)]string filePath, + internal static extern int CreateSymLink([MarshalAs(UnmanagedType.LPStr)]string filePath, [MarshalAs(UnmanagedType.LPStr)]string target); [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] - internal static extern int CreateHardLink([MarshalAs(UnmanagedType.LPStr)]string filePath, + internal static extern int CreateHardLink([MarshalAs(UnmanagedType.LPStr)]string filePath, [MarshalAs(UnmanagedType.LPStr)]string target); [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index bcdcbe496..af4acc2bf 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -1015,8 +1015,10 @@ namespace System.Management.Automation { cacheStoreLocation = Environment.GetEnvironmentVariable("PSModuleAnalysisCachePath") ?? - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - @"Microsoft\Windows\PowerShell\ModuleAnalysisCache"); + (Platform.IsWindows + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + @"Microsoft\Windows\PowerShell\ModuleAnalysisCache") + : Path.Combine(Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE), "ModuleAnalysisCache")); } } diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 129fd8db5..66f9e5ede 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -548,13 +548,20 @@ namespace System.Management.Automation /// personal module path internal static string GetPersonalModulePath() { - string personalModuleRoot = Path.Combine( - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - Utils.ProductNameForDirectory), - Utils.ModuleDirectory); - - return personalModuleRoot; + if (Platform.IsWindows) + { + string personalModuleRoot = Path.Combine( + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + Utils.ProductNameForDirectory), + Utils.ModuleDirectory); + return personalModuleRoot; + } + else + { + string personalModuleRoot = Platform.SelectProductNameForDirectory(Platform.XDG_Type.MODULES); + return personalModuleRoot; + } } /// diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 8c4f55e7d..ac23e8014 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -606,7 +606,7 @@ namespace System.Management.Automation /// Profile uses this to control profile loading. /// internal static string ProductNameForDirectory = - Platform.IsWindows ? "WindowsPowerShell" : Platform.ProductNameForDirectory; + Platform.IsWindows ? "WindowsPowerShell" : Platform.SelectProductNameForDirectory(Platform.XDG_Type.PROFILE); /// /// The name of the subdirectory that contains packages. diff --git a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs index 8783240ec..32d415965 100644 --- a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs +++ b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs @@ -198,7 +198,6 @@ namespace System.Management.Automation string profileName = useTestProfile ? "profile_test.ps1" : "profile.ps1"; - if (!string.IsNullOrEmpty(shellId)) { profileName = shellId + "_" + profileName; diff --git a/test/powershell/Base-Directory.Tests.ps1 b/test/powershell/Base-Directory.Tests.ps1 new file mode 100644 index 000000000..7e517fa8c --- /dev/null +++ b/test/powershell/Base-Directory.Tests.ps1 @@ -0,0 +1,116 @@ +Describe "Configuration file locations" { + + BeforeAll { + $powershell = Join-Path -Path $PsHome -ChildPath "powershell" + $profileName = "Microsoft.PowerShell_profile.ps1" + } + + Context "Default configuration file locations" { + + BeforeAll { + + if ($IsWindows) { + $expectedCache = [IO.Path]::Combine($env:LOCALAPPDATA, "Microsoft", "Windows", "PowerShell", "StartupProfileData-NonInteractive") + $expectedModule = [IO.Path]::Combine($env:USERPROFILE, "Documents", "WindowsPowerShell", "Modules") + $expectedProfile = [io.path]::Combine($env:USERPROFILE, "Documents","WindowsPowerShell",$profileName) + $expectedReadline = [IO.Path]::Combine($env:AppData, "Microsoft", "Windows", "PowerShell", "PSReadline", "ConsoleHost_history.txt") + } else { + $expectedCache = [IO.Path]::Combine($env:HOME, ".cache", "powershell", "StartupProfileData-NonInteractive") + $expectedModule = [IO.Path]::Combine($env:HOME, ".local", "share", "powershell", "Modules") + $expectedProfile = [io.path]::Combine($env:HOME,".config","powershell",$profileName) + $expectedReadline = [IO.Path]::Combine($env:HOME, ".local", "share", "powershell", "PSReadLine", "ConsoleHost_history.txt") + } + + if ($env:TRAVIS_OS_NAME -eq "osx") { + $ItArgs = @{ pending = $true } + } else { + $ItArgs = @{} + } + } + + BeforeEach { + $original_PSMODULEPATH = $env:PSMODULEPATH + } + + AfterEach { + $env:PSMODULEPATH = $original_PSMODULEPATH + } + + It @ItArgs "Profile location should be correct" { + & $powershell -noprofile `$PROFILE | Should Be $expectedProfile + } + + It @ItArgs "PSMODULEPATH should contain the correct path" { + $env:PSMODULEPATH = "" + $actual = & $powershell -noprofile `$env:PSMODULEPATH + $actual | Should Match ([regex]::Escape($expectedModule)) + } + + It @ItArgs "PSReadLine history save location should be correct" { + & $powershell -noprofile { (Get-PSReadlineOption).HistorySavePath } | Should Be $expectedReadline + } + + It @ItArgs "JIT cache should be created correctly" { + Remove-Item -ErrorAction SilentlyContinue $expectedCache + & $powershell -noprofile { exit } + $expectedCache | Should Exist + } + + # The ModuleAnalysisCache cannot be forced to exist, thus we cannot test it + } + + Context "XDG Base Directory Specification is supported on Linux" { + BeforeAll { + # Using It @ItArgs, we automatically skip on Windows for all these tests + if ($IsWindows) { + $ItArgs = @{ skip = $true } + } elseif ($env:TRAVIS_OS_NAME -eq "osx") { + $ItArgs = @{ pending = $true } + } else { + $ItArgs = @{} + } + } + + BeforeEach { + $original_PSMODULEPATH = $env:PSMODULEPATH + $original_XDG_CONFIG_HOME = $env:XDG_CONFIG_HOME + $original_XDG_CACHE_HOME = $env:XDG_CACHE_HOME + $original_XDG_DATA_HOME = $env:XDG_DATA_HOME + } + + AfterEach { + $env:PSMODULEPATH = $original_PSMODULEPATH + $env:XDG_CONFIG_HOME = $original_XDG_CONFIG_HOME + $env:XDG_CACHE_HOME = $original_XDG_CACHE_HOME + $env:XDG_DATA_HOME = $original_XDG_DATA_HOME + } + + It @ItArgs "Profile should respect XDG_CONFIG_HOME" { + $env:XDG_CONFIG_HOME = $TestDrive + $expected = [IO.Path]::Combine($TestDrive, "powershell", $profileName) + & $powershell -noprofile `$PROFILE | Should Be $expected + } + + It @ItArgs "PSMODULEPATH should respect XDG_DATA_HOME" { + $env:PSMODULEPATH = "" + $env:XDG_DATA_HOME = $TestDrive + $expected = [IO.Path]::Combine($TestDrive, "powershell", "Modules") + $actual = & $powershell -noprofile `$env:PSMODULEPATH + $actual | Should Match $expected + } + + It @ItArgs "PSReadLine history should respect XDG_DATA_HOME" { + $env:XDG_DATA_HOME = $TestDrive + $expected = [IO.Path]::Combine($TestDrive, "powershell", "PSReadLine", "ConsoleHost_history.txt") + & $powershell -noprofile { (Get-PSReadlineOption).HistorySavePath } | Should Be $expected + } + + It @ItArgs "JIT cache should respect XDG_CACHE_HOME" { + $env:XDG_CACHE_HOME = $TestDrive + $expected = [IO.Path]::Combine($TestDrive, "powershell", "StartupProfileData-NonInteractive") + Remove-Item -ErrorAction SilentlyContinue $expected + & $powershell -noprofile { exit } + $expected | Should Exist + } + } +}