Skip to content

Commit f6d789a

Browse files
authored
Merge pull request bonsai-rx#8 from glopesdev/linux-support
Add support for linux and system site packages
2 parents acb2244 + 95c89e7 commit f6d789a

6 files changed

Lines changed: 226 additions & 45 deletions

File tree

src/Bonsai.Scripting.Python/Bonsai.Scripting.Python.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Bonsai.Core" Version="2.7.0" />
14-
<PackageReference Include="pythonnet" Version="3.0.1" />
14+
<PackageReference Include="pythonnet" Version="3.0.3" />
1515
</ItemGroup>
1616

1717
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.IO;
2+
3+
namespace Bonsai.Scripting.Python
4+
{
5+
internal class EnvironmentConfig
6+
{
7+
private EnvironmentConfig()
8+
{
9+
}
10+
11+
public EnvironmentConfig(string pythonHome, string pythonVersion)
12+
{
13+
Path = pythonHome;
14+
PythonHome = pythonHome;
15+
PythonVersion = pythonVersion;
16+
IncludeSystemSitePackages = true;
17+
}
18+
19+
public string Path { get; private set; }
20+
21+
public string PythonHome { get; private set; }
22+
23+
public string PythonVersion { get; private set; }
24+
25+
public bool IncludeSystemSitePackages { get; private set; }
26+
27+
public static EnvironmentConfig FromConfigFile(string configFileName)
28+
{
29+
var config = new EnvironmentConfig();
30+
config.Path = System.IO.Path.GetDirectoryName(configFileName);
31+
using var configReader = new StreamReader(File.OpenRead(configFileName));
32+
while (!configReader.EndOfStream)
33+
{
34+
var line = configReader.ReadLine();
35+
static string GetConfigValue(string line)
36+
{
37+
var parts = line.Split('=');
38+
return parts.Length > 1 ? parts[1].Trim() : string.Empty;
39+
}
40+
41+
if (line.StartsWith("home"))
42+
{
43+
config.PythonHome = GetConfigValue(line);
44+
}
45+
else if (line.StartsWith("include-system-site-packages"))
46+
{
47+
config.IncludeSystemSitePackages = bool.Parse(GetConfigValue(line));
48+
}
49+
else if (line.StartsWith("version"))
50+
{
51+
var pythonVersion = GetConfigValue(line);
52+
if (!string.IsNullOrEmpty(pythonVersion))
53+
{
54+
pythonVersion = pythonVersion.Substring(0, pythonVersion.LastIndexOf('.'));
55+
}
56+
config.PythonVersion = pythonVersion;
57+
}
58+
}
59+
60+
return config;
61+
}
62+
}
63+
}
Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,132 @@
11
using System;
22
using System.IO;
3-
using System.Linq;
3+
using System.Runtime.InteropServices;
44
using Python.Runtime;
55

66
namespace Bonsai.Scripting.Python
77
{
88
static class EnvironmentHelper
99
{
10-
public static string GetPythonDLL(string path)
10+
public static string GetPythonDLL(EnvironmentConfig config)
1111
{
12-
return Directory
13-
.EnumerateFiles(path, searchPattern: "python3?*.*")
14-
.Select(Path.GetFileNameWithoutExtension)
15-
.Where(match => match.Length > "python3".Length)
16-
.Select(match => match.Replace(".", string.Empty))
17-
.FirstOrDefault();
12+
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
13+
? $"python{config.PythonVersion.Replace(".", string.Empty)}.dll"
14+
: $"libpython{config.PythonVersion}.so";
1815
}
1916

2017
public static void SetRuntimePath(string pythonHome)
2118
{
22-
var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process).TrimEnd(Path.PathSeparator);
23-
path = string.IsNullOrEmpty(path) ? pythonHome : pythonHome + Path.PathSeparator + path;
24-
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
19+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
20+
{
21+
var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process).TrimEnd(Path.PathSeparator);
22+
path = string.IsNullOrEmpty(path) ? pythonHome : pythonHome + Path.PathSeparator + path;
23+
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
24+
}
25+
}
26+
27+
static string FindPythonHome()
28+
{
29+
var systemPath = Environment.GetEnvironmentVariable("PATH");
30+
var searchPaths = systemPath.Split(Path.PathSeparator);
31+
var isRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
32+
var pythonExecutableName = isRunningOnWindows ? "python.exe" : "python3";
33+
34+
var pythonHome = Array.Find(searchPaths, path => File.Exists(Path.Combine(path, pythonExecutableName)));
35+
if (pythonHome != null && !isRunningOnWindows && MonoHelper.IsRunningOnMono)
36+
{
37+
var pythonExecutablePath = Path.Combine(pythonHome, pythonExecutableName);
38+
pythonExecutablePath = MonoHelper.GetRealPath(pythonExecutablePath);
39+
var baseDirectory = Directory.GetParent(pythonExecutablePath).Parent;
40+
if (baseDirectory != null)
41+
{
42+
pythonHome = Path.Combine(baseDirectory.FullName, "lib", Path.GetFileName(pythonExecutablePath));
43+
}
44+
}
45+
46+
return pythonHome;
47+
}
48+
49+
public static string GetEnvironmentPath(string path)
50+
{
51+
if (string.IsNullOrEmpty(path))
52+
{
53+
path = Environment.GetEnvironmentVariable("VIRTUAL_ENV", EnvironmentVariableTarget.Process);
54+
path ??= FindPythonHome();
55+
}
56+
else
57+
{
58+
path = Path.GetFullPath(path);
59+
}
60+
61+
return PathHelper.TrimEndingDirectorySeparator(path);
2562
}
2663

27-
public static string GetPythonHome(string path)
64+
public static EnvironmentConfig GetEnvironmentConfig(string path)
2865
{
2966
var configFileName = Path.Combine(path, "pyvenv.cfg");
3067
if (File.Exists(configFileName))
3168
{
32-
using var configReader = new StreamReader(File.OpenRead(configFileName));
33-
while (!configReader.EndOfStream)
69+
return EnvironmentConfig.FromConfigFile(configFileName);
70+
}
71+
else
72+
{
73+
var pythonHome = path;
74+
var pythonVersion = string.Empty;
75+
const string DefaultPythonName = "python";
76+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
3477
{
35-
var line = configReader.ReadLine();
36-
if (line.StartsWith("home"))
37-
{
38-
var parts = line.Split('=');
39-
return parts[parts.Length - 1].Trim();
40-
}
78+
var baseDirectory = Directory.GetParent(path).Parent;
79+
pythonHome = Path.Combine(baseDirectory.FullName, "bin");
4180
}
42-
}
4381

44-
return path;
82+
var pythonName = Path.GetFileName(path);
83+
var pythonVersionIndex = pythonName.LastIndexOf(DefaultPythonName, StringComparison.OrdinalIgnoreCase);
84+
if (pythonVersionIndex >= 0)
85+
{
86+
pythonVersion = pythonName.Substring(pythonVersionIndex + DefaultPythonName.Length);
87+
}
88+
89+
return new EnvironmentConfig(pythonHome, pythonVersion);
90+
}
4591
}
4692

47-
public static string GetPythonPath(string pythonHome, string path)
93+
public static string GetPythonPath(EnvironmentConfig config)
4894
{
49-
var basePath = PythonEngine.PythonPath;
50-
if (string.IsNullOrEmpty(basePath))
95+
string basePath;
96+
string sitePackages;
97+
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
98+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
5199
{
52-
var pythonZip = Path.Combine(pythonHome, Path.ChangeExtension(Runtime.PythonDLL, ".zip"));
53-
var pythonDLLs = Path.Combine(pythonHome, "DLLs");
54-
var pythonLib = Path.Combine(pythonHome, "Lib");
55-
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
100+
var pythonZip = Path.Combine(config.PythonHome, Path.ChangeExtension(Runtime.PythonDLL, ".zip"));
101+
var pythonDLLs = Path.Combine(config.PythonHome, "DLLs");
102+
var pythonLib = Path.Combine(config.PythonHome, "Lib");
56103
basePath = string.Join(Path.PathSeparator.ToString(), pythonZip, pythonDLLs, pythonLib, baseDirectory);
104+
105+
sitePackages = Path.Combine(config.Path, "Lib", "site-packages");
106+
if (config.IncludeSystemSitePackages && config.Path != config.PythonHome)
107+
{
108+
var systemSitePackages = Path.Combine(config.PythonHome, "Lib", "site-packages");
109+
sitePackages = $"{sitePackages}{Path.PathSeparator}{systemSitePackages}";
110+
}
111+
}
112+
else
113+
{
114+
var pythonBase = Path.GetDirectoryName(config.PythonHome);
115+
pythonBase = Path.Combine(pythonBase, "lib", $"python{config.PythonVersion}");
116+
var pythonLibDynload = Path.Combine(pythonBase, "lib-dynload");
117+
basePath = string.Join(Path.PathSeparator.ToString(), pythonBase, pythonLibDynload, baseDirectory);
118+
119+
sitePackages = Path.Combine(config.Path, "lib", $"python{config.PythonVersion}", "site-packages");
120+
if (config.IncludeSystemSitePackages)
121+
{
122+
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
123+
var localFolder = Directory.GetParent(localAppData).FullName;
124+
var systemSitePackages = Path.Combine(localFolder, "lib", $"python{config.PythonVersion}", "site-packages");
125+
sitePackages = $"{sitePackages}{Path.PathSeparator}{systemSitePackages}";
126+
}
57127
}
58128

59-
var sitePackages = Path.Combine(path, "Lib", "site-packages");
60-
return $"{basePath}{Path.PathSeparator}{path}{Path.PathSeparator}{sitePackages}";
129+
return $"{basePath}{Path.PathSeparator}{config.Path}{Path.PathSeparator}{sitePackages}";
61130
}
62131
}
63132
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace Bonsai.Scripting.Python
4+
{
5+
static class MonoHelper
6+
{
7+
internal static readonly bool IsRunningOnMono = Type.GetType("Mono.Runtime") != null;
8+
9+
public static string GetRealPath(string path)
10+
{
11+
var unixPath = Type.GetType("Mono.Unix.UnixPath, Mono.Posix");
12+
if (unixPath == null)
13+
{
14+
throw new InvalidOperationException("No compatible Mono.Posix implementation was found.");
15+
}
16+
17+
var getRealPath = unixPath.GetMethod(nameof(GetRealPath));
18+
if (getRealPath == null)
19+
{
20+
throw new InvalidOperationException($"No compatible {nameof(GetRealPath)} method was found.");
21+
}
22+
23+
return (string)getRealPath.Invoke(null, new[] { path });
24+
}
25+
}
26+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.IO;
2+
3+
namespace Bonsai.Scripting.Python
4+
{
5+
static class PathHelper
6+
{
7+
static bool IsDirectorySeparator(char c)
8+
{
9+
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
10+
}
11+
12+
static bool IsRoot(string path)
13+
{
14+
return !string.IsNullOrEmpty(path) &&
15+
Path.IsPathRooted(path) &&
16+
path.Length == Path.GetPathRoot(path).Length;
17+
}
18+
19+
internal static string TrimEndingDirectorySeparator(string path)
20+
{
21+
if (!string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]) && !IsRoot(path))
22+
{
23+
path = path.Substring(0, path.Length - 1);
24+
}
25+
26+
return path;
27+
}
28+
}
29+
}

src/Bonsai.Scripting.Python/RuntimeManager.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,15 @@ static void Initialize(string path)
115115
{
116116
if (!PythonEngine.IsInitialized)
117117
{
118-
if (string.IsNullOrEmpty(path))
119-
{
120-
path = Environment.GetEnvironmentVariable("VIRTUAL_ENV", EnvironmentVariableTarget.Process);
121-
if (string.IsNullOrEmpty(path)) path = Environment.CurrentDirectory;
122-
}
123-
124-
path = Path.GetFullPath(path);
125-
var pythonHome = EnvironmentHelper.GetPythonHome(path);
126-
Runtime.PythonDLL = EnvironmentHelper.GetPythonDLL(pythonHome);
127-
EnvironmentHelper.SetRuntimePath(pythonHome);
128-
PythonEngine.PythonHome = pythonHome;
129-
if (pythonHome != path)
118+
path = EnvironmentHelper.GetEnvironmentPath(path);
119+
var config = EnvironmentHelper.GetEnvironmentConfig(path);
120+
Runtime.PythonDLL = EnvironmentHelper.GetPythonDLL(config);
121+
EnvironmentHelper.SetRuntimePath(config.PythonHome);
122+
PythonEngine.PythonHome = config.PythonHome;
123+
if (config.PythonHome != path)
130124
{
131125
var version = PythonEngine.Version;
132-
PythonEngine.PythonPath = EnvironmentHelper.GetPythonPath(pythonHome, path);
126+
PythonEngine.PythonPath = EnvironmentHelper.GetPythonPath(config);
133127
}
134128
PythonEngine.Initialize();
135129
}

0 commit comments

Comments
 (0)