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
2 changes: 1 addition & 1 deletion src/Bonsai.Scripting.Python/Bonsai.Scripting.Python.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<ItemGroup>
<PackageReference Include="Bonsai.Core" Version="2.7.0" />
<PackageReference Include="pythonnet" Version="3.0.1" />
<PackageReference Include="pythonnet" Version="3.0.3" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions src/Bonsai.Scripting.Python/EnvironmentConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IO;

namespace Bonsai.Scripting.Python
{
internal class EnvironmentConfig
{
private EnvironmentConfig()
{
}

public EnvironmentConfig(string pythonHome, string pythonVersion)
{
Path = pythonHome;
PythonHome = pythonHome;
PythonVersion = pythonVersion;
IncludeSystemSitePackages = true;
}

public string Path { get; private set; }

public string PythonHome { get; private set; }

public string PythonVersion { get; private set; }

public bool IncludeSystemSitePackages { get; private set; }

public static EnvironmentConfig FromConfigFile(string configFileName)
{
var config = new EnvironmentConfig();
config.Path = System.IO.Path.GetDirectoryName(configFileName);
using var configReader = new StreamReader(File.OpenRead(configFileName));
while (!configReader.EndOfStream)
{
var line = configReader.ReadLine();
static string GetConfigValue(string line)
{
var parts = line.Split('=');
return parts.Length > 1 ? parts[1].Trim() : string.Empty;
}

if (line.StartsWith("home"))
{
config.PythonHome = GetConfigValue(line);
}
else if (line.StartsWith("include-system-site-packages"))
{
config.IncludeSystemSitePackages = bool.Parse(GetConfigValue(line));
}
else if (line.StartsWith("version"))
{
var pythonVersion = GetConfigValue(line);
if (!string.IsNullOrEmpty(pythonVersion))
{
pythonVersion = pythonVersion.Substring(0, pythonVersion.LastIndexOf('.'));
}
config.PythonVersion = pythonVersion;
}
}

return config;
}
}
}
131 changes: 100 additions & 31 deletions src/Bonsai.Scripting.Python/EnvironmentHelper.cs
Original file line number Diff line number Diff line change
@@ -1,63 +1,132 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Python.Runtime;

namespace Bonsai.Scripting.Python
{
static class EnvironmentHelper
{
public static string GetPythonDLL(string path)
public static string GetPythonDLL(EnvironmentConfig config)
{
return Directory
.EnumerateFiles(path, searchPattern: "python3?*.*")
.Select(Path.GetFileNameWithoutExtension)
.Where(match => match.Length > "python3".Length)
.Select(match => match.Replace(".", string.Empty))
.FirstOrDefault();
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"python{config.PythonVersion.Replace(".", string.Empty)}.dll"
: $"libpython{config.PythonVersion}.so";
}

public static void SetRuntimePath(string pythonHome)
{
var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process).TrimEnd(Path.PathSeparator);
path = string.IsNullOrEmpty(path) ? pythonHome : pythonHome + Path.PathSeparator + path;
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process).TrimEnd(Path.PathSeparator);
path = string.IsNullOrEmpty(path) ? pythonHome : pythonHome + Path.PathSeparator + path;
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
}
}

static string FindPythonHome()
{
var systemPath = Environment.GetEnvironmentVariable("PATH");
var searchPaths = systemPath.Split(Path.PathSeparator);
var isRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var pythonExecutableName = isRunningOnWindows ? "python.exe" : "python3";

var pythonHome = Array.Find(searchPaths, path => File.Exists(Path.Combine(path, pythonExecutableName)));
if (pythonHome != null && !isRunningOnWindows && MonoHelper.IsRunningOnMono)
{
var pythonExecutablePath = Path.Combine(pythonHome, pythonExecutableName);
pythonExecutablePath = MonoHelper.GetRealPath(pythonExecutablePath);
var baseDirectory = Directory.GetParent(pythonExecutablePath).Parent;
if (baseDirectory != null)
{
pythonHome = Path.Combine(baseDirectory.FullName, "lib", Path.GetFileName(pythonExecutablePath));
}
}

return pythonHome;
}

public static string GetEnvironmentPath(string path)
{
if (string.IsNullOrEmpty(path))
{
path = Environment.GetEnvironmentVariable("VIRTUAL_ENV", EnvironmentVariableTarget.Process);
path ??= FindPythonHome();
}
else
{
path = Path.GetFullPath(path);
}

return PathHelper.TrimEndingDirectorySeparator(path);
}

public static string GetPythonHome(string path)
public static EnvironmentConfig GetEnvironmentConfig(string path)
{
var configFileName = Path.Combine(path, "pyvenv.cfg");
if (File.Exists(configFileName))
{
using var configReader = new StreamReader(File.OpenRead(configFileName));
while (!configReader.EndOfStream)
return EnvironmentConfig.FromConfigFile(configFileName);
}
else
{
var pythonHome = path;
var pythonVersion = string.Empty;
const string DefaultPythonName = "python";
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var line = configReader.ReadLine();
if (line.StartsWith("home"))
{
var parts = line.Split('=');
return parts[parts.Length - 1].Trim();
}
var baseDirectory = Directory.GetParent(path).Parent;
pythonHome = Path.Combine(baseDirectory.FullName, "bin");
}
}

return path;
var pythonName = Path.GetFileName(path);
var pythonVersionIndex = pythonName.LastIndexOf(DefaultPythonName, StringComparison.OrdinalIgnoreCase);
if (pythonVersionIndex >= 0)
{
pythonVersion = pythonName.Substring(pythonVersionIndex + DefaultPythonName.Length);
}

return new EnvironmentConfig(pythonHome, pythonVersion);
}
}

public static string GetPythonPath(string pythonHome, string path)
public static string GetPythonPath(EnvironmentConfig config)
{
var basePath = PythonEngine.PythonPath;
if (string.IsNullOrEmpty(basePath))
string basePath;
string sitePackages;
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var pythonZip = Path.Combine(pythonHome, Path.ChangeExtension(Runtime.PythonDLL, ".zip"));
var pythonDLLs = Path.Combine(pythonHome, "DLLs");
var pythonLib = Path.Combine(pythonHome, "Lib");
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var pythonZip = Path.Combine(config.PythonHome, Path.ChangeExtension(Runtime.PythonDLL, ".zip"));
var pythonDLLs = Path.Combine(config.PythonHome, "DLLs");
var pythonLib = Path.Combine(config.PythonHome, "Lib");
basePath = string.Join(Path.PathSeparator.ToString(), pythonZip, pythonDLLs, pythonLib, baseDirectory);

sitePackages = Path.Combine(config.Path, "Lib", "site-packages");
if (config.IncludeSystemSitePackages && config.Path != config.PythonHome)
{
var systemSitePackages = Path.Combine(config.PythonHome, "Lib", "site-packages");
sitePackages = $"{sitePackages}{Path.PathSeparator}{systemSitePackages}";
}
}
else
{
var pythonBase = Path.GetDirectoryName(config.PythonHome);
pythonBase = Path.Combine(pythonBase, "lib", $"python{config.PythonVersion}");
var pythonLibDynload = Path.Combine(pythonBase, "lib-dynload");
basePath = string.Join(Path.PathSeparator.ToString(), pythonBase, pythonLibDynload, baseDirectory);

sitePackages = Path.Combine(config.Path, "lib", $"python{config.PythonVersion}", "site-packages");
if (config.IncludeSystemSitePackages)
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var localFolder = Directory.GetParent(localAppData).FullName;
var systemSitePackages = Path.Combine(localFolder, "lib", $"python{config.PythonVersion}", "site-packages");
sitePackages = $"{sitePackages}{Path.PathSeparator}{systemSitePackages}";
}
}

var sitePackages = Path.Combine(path, "Lib", "site-packages");
return $"{basePath}{Path.PathSeparator}{path}{Path.PathSeparator}{sitePackages}";
return $"{basePath}{Path.PathSeparator}{config.Path}{Path.PathSeparator}{sitePackages}";
}
}
}
26 changes: 26 additions & 0 deletions src/Bonsai.Scripting.Python/MonoHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Bonsai.Scripting.Python
{
static class MonoHelper
{
internal static readonly bool IsRunningOnMono = Type.GetType("Mono.Runtime") != null;

public static string GetRealPath(string path)
{
var unixPath = Type.GetType("Mono.Unix.UnixPath, Mono.Posix");
if (unixPath == null)
{
throw new InvalidOperationException("No compatible Mono.Posix implementation was found.");
}

var getRealPath = unixPath.GetMethod(nameof(GetRealPath));
if (getRealPath == null)
{
throw new InvalidOperationException($"No compatible {nameof(GetRealPath)} method was found.");
}

return (string)getRealPath.Invoke(null, new[] { path });
}
}
}
29 changes: 29 additions & 0 deletions src/Bonsai.Scripting.Python/PathHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.IO;

namespace Bonsai.Scripting.Python
{
static class PathHelper
{
static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}

static bool IsRoot(string path)
{
return !string.IsNullOrEmpty(path) &&
Path.IsPathRooted(path) &&
path.Length == Path.GetPathRoot(path).Length;
}

internal static string TrimEndingDirectorySeparator(string path)
{
if (!string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]) && !IsRoot(path))
{
path = path.Substring(0, path.Length - 1);
}

return path;
}
}
}
20 changes: 7 additions & 13 deletions src/Bonsai.Scripting.Python/RuntimeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,15 @@ static void Initialize(string path)
{
if (!PythonEngine.IsInitialized)
{
if (string.IsNullOrEmpty(path))
{
path = Environment.GetEnvironmentVariable("VIRTUAL_ENV", EnvironmentVariableTarget.Process);
if (string.IsNullOrEmpty(path)) path = Environment.CurrentDirectory;
}

path = Path.GetFullPath(path);
var pythonHome = EnvironmentHelper.GetPythonHome(path);
Runtime.PythonDLL = EnvironmentHelper.GetPythonDLL(pythonHome);
EnvironmentHelper.SetRuntimePath(pythonHome);
PythonEngine.PythonHome = pythonHome;
if (pythonHome != path)
path = EnvironmentHelper.GetEnvironmentPath(path);
var config = EnvironmentHelper.GetEnvironmentConfig(path);
Runtime.PythonDLL = EnvironmentHelper.GetPythonDLL(config);
EnvironmentHelper.SetRuntimePath(config.PythonHome);
PythonEngine.PythonHome = config.PythonHome;
if (config.PythonHome != path)
{
var version = PythonEngine.Version;
PythonEngine.PythonPath = EnvironmentHelper.GetPythonPath(pythonHome, path);
PythonEngine.PythonPath = EnvironmentHelper.GetPythonPath(config);
}
PythonEngine.Initialize();
}
Expand Down