GitHub Viewer
namespace LostTech.WhichPython {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.Win32;
using static System.FormattableString;
///
/// Provides information about a Python environment
///
public class PythonEnvironment {
///
/// Path to Python executable.
/// Usually python.exe on Windows and pythonX.X on Linux
///
public FileSystemInfo? InterpreterPath { get; }
///
/// The value of interpreter's PYTHONHOME
///
public FileSystemInfo? Home { get; }
///
/// Path to the Python dynamic library (useful for embedding). E.g. python36.dll on Windows.
///
public FileSystemInfo? DynamicLibraryPath { get; }
///
/// Python language version.
///
public Version? LanguageVersion { get; }
///
/// Interpreter architecture.
///
public Architecture? Architecture { get; }
///
/// Creates new instance of
///
/// Path to Python executable.
/// Usually python.exe on Windows and pythonX.X on Linux.
/// The value of interpreter's PYTHONHOME. Optional.
/// Path to the Python dynamic library (useful for embedding). Optional.
/// Python language version. Optional.
/// Interpreter architecture
public PythonEnvironment(FileSystemInfo? interpreterPath, FileSystemInfo? home, FileSystemInfo? dll, Version? languageVersion, Architecture? architecture) {
this.InterpreterPath = interpreterPath;
this.Home = home;
this.DynamicLibraryPath = dll;
this.LanguageVersion = languageVersion;
this.Architecture = architecture;
}
///
/// Enumerate Python interpreters, installed for current user (including system-wide ones).
/// SECURITY: this invokes any files matching python* mask in PATH to determine if they are a Python interpreter.
///
public static IEnumerable EnumerateEnvironments(CancellationToken cancellation = default) {
var found = new SortedSet();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
foreach (var environment in EnumerateRegistryEnvironments(found, cancellation))
yield return environment;
}
foreach (var environment in EnumeratePathEnvironments(found, cancellation))
yield return environment;
}
///
/// Enumerate Conda environments of the current user.
///
[Obsolete("Use CondaEnvironment.EnumerateCondaEnvironments")]
public static IEnumerable EnumerateCondaEnvironments(CancellationToken cancellation = default)
=> CondaEnvironment.EnumerateCondaEnvironments(cancellation);
///
/// Enumerate Python interpreters in the specified directories.
/// Does not go into subdirectories.
/// SECURITY: this invokes any files matching the mask to determine if they are a Python interpreter.
///
/// Directories to search for interpreters.
/// What an interpreter file name is expected to look like.
/// Optional cancellation token.
public static IEnumerable EnumeratePathEnvironments(
IEnumerable paths,
string interpreterFileNameMask = "python*",
CancellationToken cancellation = default){
if (paths == null) throw new ArgumentNullException(nameof(paths));
if (interpreterFileNameMask == null) throw new ArgumentNullException(nameof(interpreterFileNameMask));
return Impl();
IEnumerable Impl() {
foreach (var directory in paths) {
if (!directory.Exists)
continue;
foreach (FileInfo potentialInterpreter in directory.EnumerateFiles(searchPattern: interpreterFileNameMask)) {
cancellation.ThrowIfCancellationRequested();
var env = TryDetectEnvironmentFromInterpreter(potentialInterpreter, cancellation);
if (env != null)
yield return env;
}
}
}
}
public static PythonEnvironment FromInterpreterChecked(FileInfo interpreter) {
if (interpreter is null) throw new ArgumentNullException(nameof(interpreter));
if (!interpreter.Exists)
throw new FileNotFoundException("Interpreter file not found", interpreter.FullName);
var version = TryGetVersion(interpreter);
if (version is null)
throw new NotSupportedException("Unable to get interpreter version");
DirectoryInfo? home = null;
string? dllPath = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
home = new DirectoryInfo(interpreter.DirectoryName);
dllPath = GetDll(home, version);
if (dllPath != null && !File.Exists(dllPath))
throw new NotSupportedException("Unable to find dynamic library");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
dllPath = LocateSo(interpreter);
if (dllPath != null) {
home = TryGuessUnixHome(interpreter, version, dllPath: dllPath);
}
}
return new PythonEnvironment(interpreter, home,
dll: dllPath is null ? null : new FileInfo(dllPath),
languageVersion: version,
architecture: null);
}
///
/// Enumerate Python environments, listed in the PATH environment variable.
///
static IEnumerable EnumeratePathEnvironments(ISet enumerated, CancellationToken cancellation) {
bool windows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
char pathSeparator = windows ? ';' : ':';
DirectoryInfo[]? paths = Environment.GetEnvironmentVariable("PATH")
?.Split(new[] { pathSeparator }, StringSplitOptions.RemoveEmptyEntries)
.Select(dirPath => new DirectoryInfo(dirPath))
.ToArray();
if (paths == null)
yield break;
string[] interpreterFileNameMasks = windows
? new[] { "python.exe" }
: new[] { "python?.?", "python" };
foreach (var env in interpreterFileNameMasks.SelectMany(
mask => EnumeratePathEnvironments(paths, mask, cancellation))) {
if (enumerated.Add(env.DynamicLibraryPath?.FullName))
yield return env;
}
}
static IEnumerable EnumerateRegistryEnvironments(ISet enumerated, CancellationToken cancellation) {
foreach (var environment in EnumerateRegistryEnvironments(Registry.CurrentUser, enumerated, cancellation))
yield return environment;
foreach (var environment in EnumerateRegistryEnvironments(Registry.LocalMachine, enumerated, cancellation))
yield return environment;
}
static IEnumerable EnumerateRegistryEnvironments(RegistryKey hive, ISet enumerated, CancellationToken cancellation) {
using var registryEnvironments = hive.OpenSubKey(@"SOFTWARE\Python\PythonCore");
if (registryEnvironments == null) yield break;
var abi = RuntimeInformation.ProcessArchitecture;
foreach (var environment in EnumerateRegistryEnvironments(enumerated, registryEnvironments, abi, cancellation))
yield return environment;
}
static IEnumerable EnumerateRegistryEnvironments(ISet enumerated, RegistryKey registryKey, Architecture abi, CancellationToken cancellation) {
foreach (string majorVersion in registryKey.GetSubKeyNames()) {
cancellation.ThrowIfCancellationRequested();
using var installPath = registryKey.OpenSubKey(majorVersion + @"\InstallPath");
if (installPath == null)
continue;
if (!(installPath.GetValue("") is string homePath)) continue;
var home = new DirectoryInfo(homePath);
string interpreterPath = Path.Combine(homePath, "python.exe");
if (!File.Exists(interpreterPath)) continue;
if (!enumerated.Add(homePath)) continue;
bool hasVer = Version.TryParse(majorVersion, out var version);
string? dllPath = hasVer ? GetDll(home, version) : null;
var dll = dllPath == null || !File.Exists(dllPath)
? null
: new FileInfo(dllPath);
yield return new PythonEnvironment(
interpreterPath: new FileInfo(interpreterPath),
home: home,
dll: dll,
version, abi);
}
}
static string WindowsGetDll(DirectoryInfo home, Version version)
=> Path.Combine(home.FullName, Invariant($"python{version.Major}{version.Minor}.dll"));
static string UnixGetDll(DirectoryInfo home, Version version) {
string mVersion = Path.Combine(home.FullName, "lib",
Invariant($"libpython{version.Major}.{version.Minor}m{DynamicLibraryExtension}"));
if (File.Exists(mVersion)) return mVersion;
return Path.Combine(home.FullName, "lib",
Invariant($"libpython{version.Major}.{version.Minor}{DynamicLibraryExtension}"));
}
internal static string GetDll(DirectoryInfo home, Version version)
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? WindowsGetDll(home, version)
: UnixGetDll(home, version);
static readonly string DynamicLibraryExtension =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
: ".so";
static Version? WindowsGetVersion(DirectoryInfo home, CancellationToken cancellation) {
return home.EnumerateFiles(searchPattern: "python??.dll")
.Concat(home.EnumerateFiles(searchPattern: "python???.dll"))
.Select(file => {
string name = Path.GetFileNameWithoutExtension(file.Name);
string verString = name.Substring("python".Length);
int major = verString[0] - '0';
if (major = 10) return null;
return int.TryParse(verString.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture, out int minor)
? new Version(major, minor)
: null;
}).WithCancellation(cancellation).Min();
}
static readonly Regex LibPythonVersionRegex = new Regex(@"^libpython(?[0-9])\.(?[0-9]+)");
static Version? UnixGetVersion(DirectoryInfo home, CancellationToken cancellation) {
if (home is null) throw new ArgumentNullException(nameof(home));
string lib = Path.Combine(home.FullName, "lib");
string extension = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" : ".so";
return Directory.EnumerateFiles(lib, searchPattern: "libpython?.*" + extension)
.Select(name => {
name = Path.GetFileNameWithoutExtension(name);
var match = LibPythonVersionRegex.Match(name);
if (!match.Success) return null;
int major = int.Parse(match.Groups["major"].Value, NumberStyles.None, CultureInfo.InvariantCulture);
int minor = int.Parse(match.Groups["minor"].Value, NumberStyles.None, CultureInfo.InvariantCulture);
return new Version(major, minor);
}).WithCancellation(cancellation).Min();
}
internal static Version? TryGetVersion(DirectoryInfo home, CancellationToken cancellation)
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? WindowsGetVersion(home, cancellation)
: UnixGetVersion(home, cancellation);
static readonly Regex FileNameVersionRegex = new Regex(@"[a-zA-Z]+(?\d\.\d)(\.exe)?");
static PythonEnvironment? TryDetectEnvironmentFromInterpreter(FileInfo potentialInterpreter, CancellationToken cancellation = default) {
if (potentialInterpreter is null) throw new ArgumentNullException(nameof(potentialInterpreter));
if (!potentialInterpreter.Exists) return null;
cancellation.ThrowIfCancellationRequested();
var version = TryGetVersion(potentialInterpreter);
if (version is null) return null;
DirectoryInfo? home = null;
string? dllPath = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
home = new DirectoryInfo(potentialInterpreter.DirectoryName);
dllPath = GetDll(home, version);
if (dllPath != null && !File.Exists(dllPath))
dllPath = null;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX)){
dllPath = TryLocateSo(potentialInterpreter);
if (dllPath != null)
home = TryGuessUnixHome(potentialInterpreter, version, dllPath: dllPath);
}
return home != null || dllPath != null
? new PythonEnvironment(interpreterPath: potentialInterpreter,
home: home,
dll: dllPath is null ? null : new FileInfo(dllPath),
languageVersion: version, architecture: null)
: null;
}
static Version? TryGetVersion(FileInfo interpreter) {
string? versionString = TryGetVersionString(interpreter);
if (versionString is null) return null;
return Version.TryParse(versionString, out var version) ? version : null;
}
static string? TryGetVersionString(FileInfo interpreter) {
var match = FileNameVersionRegex.Match(interpreter.Name);
if (match.Success)
return match.Groups["ver"].Value;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && interpreter.Name == "python"
|| RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && interpreter.Name == "python.exe")
return TryRunScript(interpreter, PrintVersionScript);
return null;
}
static string? TryRunScript(FileInfo interpreter, string script, int timeoutMilliseconds = 5000) {
var startInfo = new ProcessStartInfo(interpreter.FullName, $"-c \"{script}\"") {
CreateNoWindow = true,
RedirectStandardOutput = true,
UseShellExecute = false,
};
try {
var process = Process.Start(startInfo);
if (!process.WaitForExit(timeoutMilliseconds)) {
process.Kill();
return null;
}
return process.StandardOutput.ReadToEnd();
} catch (PlatformNotSupportedException) {
return null;
} catch (System.ComponentModel.Win32Exception notFound) when (notFound.NativeErrorCode == 2) {
return null;
}
}
static string LocateSo(FileInfo interpreterPath) {
string? libPathsOutput = TryGetLibPaths(interpreterPath);
if (libPathsOutput is null)
throw new NotSupportedException("Unable to get lib paths");
string? so = TryGetDllFromGetLibPathsOutput(libPathsOutput);
return so ?? throw new NotImplementedException("Unable to find DLL in lib paths: " + libPathsOutput);
}
static string? TryLocateSo(FileInfo interpreterPath) {
string? libPathsOutput = TryGetLibPaths(interpreterPath);
if (libPathsOutput is null) return null;
return TryGetDllFromGetLibPathsOutput(libPathsOutput);
}
static string? TryGetLibPaths(FileInfo interpreterPath) {
if (interpreterPath is null) throw new ArgumentNullException(nameof(interpreterPath));
string getLibPathsScript = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? LinuxGetLibPathsScript
: MacGetLibPathsScript;
return TryRunScript(interpreterPath, getLibPathsScript);
}
static string? TryGetDllFromGetLibPathsOutput(string stdout) {
string[] paths = stdout
.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
.ToArray();
return paths.Select(libPath => Path.ChangeExtension(libPath, DynamicLibraryExtension))
.Concat(paths)
.Where(libPath => Path.GetExtension(libPath) != ".a")
.FirstOrDefault(File.Exists);
}
static DirectoryInfo? TryGuessUnixHome(FileInfo interpreterPath, Version version, string dllPath) {
var candidate = interpreterPath.Directory;
var dllDir = new FileInfo(dllPath).Directory;
if (IsSubDirectoryOrSame(parent: candidate, child: dllDir))
return candidate;
if (candidate.Name == "bin" && candidate.Parent != null) {
candidate = candidate.Parent;
if (IsSubDirectoryOrSame(candidate, child: dllDir))
return candidate;
}
string? sysPaths = TryRunScript(interpreterPath, PrintSysPathScript);
if (sysPaths != null) {
var paths = sysPaths
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.Select(path => new DirectoryInfo(path))
.ToArray();
var home = paths.FirstOrDefault(path =>
path.Name == Invariant($"python{version.Major}.{version.Minor}")
&& path.Parent != null && path.Parent.Name == "lib")?.Parent?.Parent;
if (home != null && IsSubDirectoryOrSame(home, child: interpreterPath.Directory))
return home;
}
return null;
}
static bool IsSubDirectoryOrSame(DirectoryInfo parent, DirectoryInfo child) {
if (child.FullName.Length < parent.FullName.Length) return false;
if ((child.FullName + Path.DirectorySeparatorChar).StartsWith(parent.FullName + Path.DirectorySeparatorChar, StringComparison.Ordinal))
return true;
return !(child.Parent is null) && IsSubDirectoryOrSame(parent, child.Parent);
}
const string PrintVersionScript = "import sys; print(str(sys.version_info[0]) + '.' + str(sys.version_info[1])+ '.' + str(sys.version_info[2]))";
const string PrintSysPathScript = @"import sys; print('\n'.join(sys.path))";
const string LinuxGetLibPathsScript = "from distutils import sysconfig; import os.path as op; v = sysconfig.get_config_vars(); fpaths = [op.join(v[pv], v['LDLIBRARY']) for pv in ('LIBDIR', 'LIBPL')]; print(list(filter(op.exists, fpaths))[0])";
const string MacGetLibPathsScript = "from distutils import sysconfig; import os.path as op; v = sysconfig.get_config_vars(); fpaths = [op.join(v[pv], fname) for pv in ('LIBDIR', 'LIBPL') for fname in (op.splitext(v['LIBRARY'])[0] + '.dylib', v['LIBRARY'])]; print(list(filter(op.exists, fpaths))[0])";
}
}