Use GetCurrentPackageFamilyNameNative API instead of path matching to detect MSIX installation (#28029)

This commit is contained in:
Dongbo Wang authored and GitHub committed 2026-10-09 09:32:47 -07:00
1 parent c172d736a6
commit 29a743d40f
3 files changed
+88 -46

No files matched your search

@@ -378,20 +378,41 @@ namespace Microsoft.PowerShell
#if UNIX
const string pwshName = "pwsh";
const string dotnetToolPathSegment = "/.store/powershell/";
const StringComparison procNameCompType = StringComparison.Ordinal;
#else
const string pwshName = "pwsh.exe";
const string dotnetToolPathSegment = @"\.store\powershell\";
const StringComparison procNameCompType = StringComparison.OrdinalIgnoreCase;
#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))
// Use 'Environment.ProcessPath' if it points to 'pwsh.exe' or 'pwsh'. The 'ProcessPath' on Windows
// could be any case as it depends on the `lpCommandLine` argument passed to `CreateProcess`, so we
// compare with 'OrdinalIgnoreCase' on Windows.
if (pwshName.Equals(processName, procNameCompType))
{
#if !UNIX
psExeHome = ResolveStablePathIfMsix(psExeHome);
// When running from an MSIX package, '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".
if (Utils.IsMSIXInstallation(out string packageFamilyName))
{
string localAppDataDir = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify);
psExeHome = $"{localAppDataDir}\\Microsoft\\WindowsApps\\{packageFamilyName}";
}
#endif
return psExeHome;
}
@@ -439,49 +460,6 @@ namespace Microsoft.PowerShell
}
}
#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>
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Runtime.InteropServices;
internal static partial class Interop
{
internal static partial class Windows
{
[LibraryImport("kernel32.dll", EntryPoint = "GetCurrentPackageFamilyName", StringMarshalling = StringMarshalling.Utf16)]
private static partial int GetCurrentPackageFamilyNameNative(ref uint packageFamilyNameLength, Span<char> packageFamilyName);
/// <summary>
/// Returns the package family name of the current process when it has package (MSIX) identity; otherwise null.
/// </summary>
internal static string? GetCurrentPackageFamilyName()
{
const int ErrorSuccess = 0;
const int PackageFamilyNameMaxLength = 65; // Maximum name plus null terminator.
try
{
uint length = PackageFamilyNameMaxLength;
Span<char> buffer = stackalloc char[PackageFamilyNameMaxLength];
int result = GetCurrentPackageFamilyNameNative(ref length, buffer);
if (result is not ErrorSuccess || length is 0)
{
return null;
}
// The returned length includes the null terminator, which we don't want in the managed string.
return new string(buffer[..(int)(length - 1)]);
}
catch (Exception exception) when (exception is DllNotFoundException or EntryPointNotFoundException)
{
return null;
}
}
}
}
@@ -475,6 +475,27 @@ namespace System.Management.Automation
return string.Empty;
}
private static string s_packageFamilyName;
private static bool? s_isMSIXInstallation;
/// <summary>
/// Determine whether the current process has package (MSIX) identity, caching the result
/// so the native API is only called once.
/// </summary>
/// <param name="packageFamilyName">Receives the package family name when the process has package identity; otherwise null.</param>
/// <returns>True if the current process has package (MSIX) identity; otherwise false.</returns>
internal static bool IsMSIXInstallation(out string packageFamilyName)
{
if (s_isMSIXInstallation is null)
{
s_packageFamilyName = Interop.Windows.GetCurrentPackageFamilyName();
s_isMSIXInstallation = s_packageFamilyName is not null;
}
packageFamilyName = s_packageFamilyName;
return s_isMSIXInstallation.Value;
}
#endif
internal static string DefaultPowerShellAppBase => GetApplicationBase(DefaultPowerShellShellID);