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
102 changes: 86 additions & 16 deletions src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,31 +118,21 @@ internal static int Start(
throw new ConsoleHostStartupException(ConsoleHostStrings.ShellCannotBeStartedWithConfigConflict);
}

// Put PSHOME in front of PATH so that calling `pwsh` within `pwsh` always starts the same running version.
// Put pwsh executable home in front of PATH so that calling `pwsh` within `pwsh` always starts the same running version.
string psExeHome = GetPSExecutableHome();
string path = Environment.GetEnvironmentVariable("PATH");
string pshome = Utils.DefaultPowerShellAppBase;
string dotnetToolsPathSegment = $"{Path.DirectorySeparatorChar}.store{Path.DirectorySeparatorChar}powershell{Path.DirectorySeparatorChar}";

int index = pshome.IndexOf(dotnetToolsPathSegment, StringComparison.Ordinal);
if (index > 0)
{
// We're running PowerShell global tool. In this case the real entry executable should be the 'pwsh'
// or 'pwsh.exe' within the tool folder which should be the path right before the '\.store', not what
// PSHome is pointing to.
pshome = pshome[0..index];
}

pshome += Path.PathSeparator;
psExeHome += Path.PathSeparator;

// To not impact startup perf, we don't remove duplicates, but we avoid adding a duplicate to the front
// we also don't handle the edge case where PATH only contains $PSHOME
if (string.IsNullOrEmpty(path))
{
Environment.SetEnvironmentVariable("PATH", pshome);
Environment.SetEnvironmentVariable("PATH", psExeHome);
}
else if (!path.StartsWith(pshome, StringComparison.Ordinal))
else if (!path.StartsWith(psExeHome, StringComparison.Ordinal))
{
Environment.SetEnvironmentVariable("PATH", pshome + path);
Environment.SetEnvironmentVariable("PATH", psExeHome + path);
}

try
Expand Down Expand Up @@ -383,6 +373,43 @@ internal static void ParseCommandLine(string[] args)

private static readonly CommandLineParameterParser s_cpp = new CommandLineParameterParser();

private static string GetPSExecutableHome()
{
#if UNIX
const string pwshName = "pwsh";
const string dotnetToolPathSegment = "/.store/powershell/";
#else
const string pwshName = "pwsh.exe";
const string dotnetToolPathSegment = @"\.store\powershell\";
#endif

string psExePath = Environment.ProcessPath;
string psExeHome = Path.GetDirectoryName(psExePath);
string processName = Path.GetFileName(psExePath);

// Use 'Environment.ProcessPath' if it points to 'pwsh.exe' or 'pwsh'.
if (pwshName.Equals(processName, StringComparison.Ordinal))
{
#if !UNIX
psExeHome = ResolveStablePathIfMsix(psExeHome);
#endif
return psExeHome;
}

psExeHome = Utils.DefaultPowerShellAppBase;

int index = psExeHome.IndexOf(dotnetToolPathSegment, StringComparison.Ordinal);
if (index > 0)
{
// We're running PowerShell dotnet tool. In this case the real entry executable should be the 'pwsh'
// or 'pwsh.exe' within the tool folder which should be the path right before the '\.store', because
// the pwsh executable under $PSHOME is an x86-64 binary that won't work on non-x86/64 platforms.
return psExeHome[0..index];
}

return psExeHome;
}

#if UNIX
/// <summary>
/// The break handler for the program. Dispatches a break event to the current Executor.
Expand Down Expand Up @@ -412,6 +439,49 @@ private static void MyBreakHandler(object sender, ConsoleCancelEventArgs args)
}
}
#else
/// <summary>
/// Handle the MSIX package scenario where <paramref name="psExeHome"/> points to the MSIX package folder under "Program Files".
///
/// That path contains a version string and will change with every update. Prepending that path to the PATH environment variable
/// caused a problem for CMake-based build systems: CMake cached the path to 'pwsh.exe' when running for the first time from the
/// MSIX PowerShell. That cached path became invalid after the MSIX PowerShell was updated, which broke CMake.
///
/// So, instead of using the "Program Files" package folder path, we need to use the stable path that contains the execution alias
/// for the specific MSIX package, e.g. use "%LOCALAPPDATA%\Microsoft\WindowsApps\Microsoft.PowerShell_8wekyb3d8bbwe" instead of
/// "%ProgramFiles%\WindowsApps\Microsoft.PowerShell_7.x.x.0_x64__8wekyb3d8bbwe".
/// </summary>
/// <param name="psExeHome">Path to the directory that contains the pwsh executable.</param>
private static string ResolveStablePathIfMsix(string psExeHome)
{
const string msixPublisherSuffix = "_8wekyb3d8bbwe";
const string msixPackageBaseName = "Microsoft.PowerShell";

if (psExeHome.EndsWith(msixPublisherSuffix, StringComparison.Ordinal))
{
string programFileDir = Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles,
Environment.SpecialFolderOption.DoNotVerify);

string prefix = $"{programFileDir}\\WindowsApps\\{msixPackageBaseName}";
if (psExeHome.StartsWith(prefix, StringComparison.Ordinal))
{
int startIndex = prefix.Length;
int underbarIndex = psExeHome.IndexOf('_', startIndex);
if (underbarIndex > 0)
{
ReadOnlySpan<char> channelSuffix = psExeHome.AsSpan(startIndex, underbarIndex - startIndex);
string localAppDataDir = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify);

psExeHome = $"{localAppDataDir}\\Microsoft\\WindowsApps\\{msixPackageBaseName}{channelSuffix}{msixPublisherSuffix}";
}
}
}

return psExeHome;
}

/// <summary>
/// The break handler for the program. Dispatches a break event to the current Executor.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions test/powershell/Host/ConsoleHost.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -718,8 +718,8 @@ $powershell -c '[System.Management.Automation.Platform]::SelectProductNameForDir
}

Context "PATH environment variable" {
It "`$PSHOME should be in front so that pwsh.exe starts current running PowerShell" {
& $powershell -v | Should -Match $PSVersionTable.GitCommitId
It "Running 'pwsh' should start the currently running PowerShell" {
pwsh -v | Should -Match $PSVersionTable.GitCommitId
}

It "powershell starts if PATH is not set" -Skip:($IsWindows) {
Expand Down
Loading