🌐 AI搜索 & 代理 主页
Skip to content

Commit 5335a34

Browse files
committed
Implement venv derivation
1 parent 9e6ffba commit 5335a34

File tree

3 files changed

+192
-52
lines changed

3 files changed

+192
-52
lines changed

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0</TargetFrameworks>
44
<Platforms>AnyCPU</Platforms>
5-
<LangVersion>10.0</LangVersion>
5+
<LangVersion>10</LangVersion>
66
<RootNamespace>Python.Runtime</RootNamespace>
77
<AssemblyName>Python.Runtime</AssemblyName>
88
<Nullable>enable</Nullable>

src/runtime/Runtime.cs

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,20 @@ namespace Python.Runtime
1919
/// </summary>
2020
public unsafe partial class Runtime
2121
{
22+
internal static PythonEnvironment PythonEnvironment = PythonEnvironment.FromEnv();
23+
2224
public static string? PythonDLL
2325
{
2426
get => _PythonDll;
2527
set
2628
{
2729
if (_isInitialized)
2830
throw new InvalidOperationException("This property must be set before runtime is initialized");
29-
_PythonDll = value;
31+
PythonEnvironment.LibPython = value;
3032
}
3133
}
3234

33-
static string? _PythonDll = GetDefaultDllName();
34-
private static string? GetDefaultDllName()
35-
{
36-
string dll = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");
37-
if (!string.IsNullOrEmpty(dll))
38-
return dll;
39-
40-
string verString = Environment.GetEnvironmentVariable("PYTHONNET_PYVER");
41-
if (!Version.TryParse(verString, out var version)) return null;
42-
43-
return GetDefaultDllName(version);
44-
}
45-
46-
private static string GetDefaultDllName(Version version)
47-
{
48-
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
49-
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
50-
? Invariant($"{version.Major}{version.Minor}")
51-
: Invariant($"{version.Major}.{version.Minor}");
52-
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
53-
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
54-
: ".so";
55-
return prefix + "python" + suffix + ext;
56-
}
35+
static string? _PythonDll => PythonEnvironment.LibPython;
5736

5837
private static bool _isInitialized = false;
5938
internal static bool IsInitialized => _isInitialized;
@@ -103,35 +82,11 @@ static void EnsureProgramName()
10382
if (!string.IsNullOrEmpty(PythonEngine.ProgramName))
10483
return;
10584

106-
string fromEnv = Environment.GetEnvironmentVariable("PYTHONNET_PYEXE");
107-
if (!string.IsNullOrEmpty(fromEnv))
85+
if (PythonEnvironment.IsValid)
10886
{
109-
PythonEngine.ProgramName = fromEnv;
87+
PythonEngine.ProgramName = PythonEnvironment.ProgramName!;
11088
return;
11189
}
112-
113-
string venv = Environment.GetEnvironmentVariable("VIRTUAL_ENV");
114-
if (!string.IsNullOrEmpty(venv))
115-
{
116-
if (IsWindows)
117-
{
118-
var path = Path.Combine(venv, "Scripts", "python.exe");
119-
if (System.IO.File.Exists(path))
120-
{
121-
PythonEngine.ProgramName = path;
122-
return;
123-
}
124-
}
125-
else
126-
{
127-
var path = Path.Combine(venv, "bin", "python");
128-
if (System.IO.File.Exists(path))
129-
{
130-
PythonEngine.ProgramName = path;
131-
return;
132-
}
133-
}
134-
}
13590
}
13691

13792
internal static bool HostedInPython;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System;
2+
using System.IO;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using static System.FormattableString;
7+
8+
namespace Python.Runtime;
9+
10+
11+
internal class PythonEnvironment
12+
{
13+
readonly static string PYDLL_ENV_VAR = "PYTHONNET_PYDLL";
14+
readonly static string PYEXE_ENV_VAR = "PYTHONNET_PYEXE";
15+
readonly static string PYNET_VENV_ENV_VAR = "PYTHONNET_VENV";
16+
readonly static string VENV_ENV_VAR = "VIRTUAL_ENV";
17+
18+
public string? VenvPath { get; private set; }
19+
public string? Home { get; private set; }
20+
public Version? Version { get; private set; }
21+
public string? ProgramName { get; set; }
22+
public string? LibPython { get; set; }
23+
24+
public bool IsValid =>
25+
!string.IsNullOrEmpty(ProgramName) && !string.IsNullOrEmpty(LibPython);
26+
27+
28+
// TODO: Move the lib-guessing step to separate function, use together with
29+
// PYTHONNET_PYEXE or a path lookup as last resort
30+
31+
// Initialize PythonEnvironment instance from environment variables.
32+
//
33+
// If PYTHONNET_PYEXE and PYTHONNET_PYDLL are set, these always have precedence.
34+
// If PYTHONNET_VENV or VIRTUAL_ENV is set, we interpret the environment as a venv
35+
// and set the ProgramName/LibPython accordingly. PYTHONNET_VENV takes precedence.
36+
public static PythonEnvironment FromEnv()
37+
{
38+
var pydll = Environment.GetEnvironmentVariable(PYDLL_ENV_VAR);
39+
var pydllSet = !string.IsNullOrEmpty(pydll);
40+
var pyexe = Environment.GetEnvironmentVariable(PYEXE_ENV_VAR);
41+
var pyexeSet = !string.IsNullOrEmpty(pyexe);
42+
var pynetVenv = Environment.GetEnvironmentVariable(PYNET_VENV_ENV_VAR);
43+
var pynetVenvSet = !string.IsNullOrEmpty(pynetVenv);
44+
var venv = Environment.GetEnvironmentVariable(VENV_ENV_VAR);
45+
var venvSet = !string.IsNullOrEmpty(venv);
46+
47+
PythonEnvironment? res = new();
48+
49+
if (pynetVenvSet)
50+
res = FromVenv(pynetVenv) ?? res;
51+
else if (venvSet)
52+
res = FromVenv(venv) ?? res;
53+
54+
if (pyexeSet)
55+
res.ProgramName = pyexe;
56+
57+
if (pydllSet)
58+
res.LibPython = pydll;
59+
60+
return res;
61+
}
62+
63+
public static PythonEnvironment? FromVenv(string path)
64+
{
65+
var env = new PythonEnvironment
66+
{
67+
VenvPath = path
68+
};
69+
70+
string venvCfg = Path.Combine(path, "pyvenv.cfg");
71+
72+
if (!File.Exists(venvCfg))
73+
return null;
74+
75+
var settings = TryParse(venvCfg);
76+
77+
if (!settings.ContainsKey("home"))
78+
return null;
79+
80+
env.Home = settings["home"];
81+
var pname = ProgramNameFromPath(path);
82+
if (File.Exists(pname))
83+
env.ProgramName = pname;
84+
85+
if (settings.TryGetValue("version", out string versionStr))
86+
{
87+
_ = Version.TryParse(versionStr, out Version versionObj);
88+
env.Version = versionObj;
89+
}
90+
91+
env.LibPython = FindLibPython(env.Home, env.Version);
92+
93+
return env;
94+
}
95+
96+
private static Dictionary<string, string> TryParse(string venvCfg)
97+
{
98+
var settings = new Dictionary<string, string>();
99+
100+
string[] lines = File.ReadAllLines(venvCfg);
101+
102+
// The actually used format is really primitive: "<key> = <value>"
103+
foreach (string line in lines)
104+
{
105+
var split = line.Split(new[] { '=' }, 2);
106+
107+
if (split.Length != 2)
108+
continue;
109+
110+
settings[split[0].Trim()] = split[1].Trim();
111+
}
112+
113+
return settings;
114+
}
115+
116+
private static string? FindLibPython(string home, Version? maybeVersion)
117+
{
118+
// TODO: Check whether there is a .dll/.so/.dylib next to the executable
119+
120+
if (maybeVersion is Version version)
121+
{
122+
return FindLibPythonInHome(home, version);
123+
}
124+
125+
return null;
126+
}
127+
128+
private static string? FindLibPythonInHome(string home, Version version)
129+
{
130+
var libPythonName = GetDefaultDllName(version);
131+
132+
List<string> pathsToCheck = new();
133+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
134+
{
135+
var arch = RuntimeInformation.ProcessArchitecture;
136+
if (arch == Architecture.X64 || arch == Architecture.Arm64)
137+
{
138+
// multilib systems
139+
pathsToCheck.Add("../lib64");
140+
}
141+
pathsToCheck.Add("../lib");
142+
}
143+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
144+
{
145+
pathsToCheck.Add(".");
146+
}
147+
else
148+
{
149+
pathsToCheck.Add("../lib");
150+
}
151+
152+
return pathsToCheck.FirstOrDefault(path =>
153+
{
154+
var p = Path.Combine(home, path, libPythonName);
155+
return File.Exists(p);
156+
});
157+
}
158+
159+
private static string ProgramNameFromPath(string path)
160+
{
161+
if (Runtime.IsWindows)
162+
{
163+
return Path.Combine(path, "Scripts", "python.exe");
164+
}
165+
else
166+
{
167+
return Path.Combine(path, "bin", "python");
168+
}
169+
}
170+
171+
internal static string GetDefaultDllName(Version version)
172+
{
173+
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
174+
175+
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
176+
? Invariant($"{version.Major}{version.Minor}")
177+
: Invariant($"{version.Major}.{version.Minor}");
178+
179+
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
180+
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
181+
: ".so";
182+
183+
return prefix + "python" + suffix + ext;
184+
}
185+
}

0 commit comments

Comments
 (0)