Skip to content

Commit 3a3abb9

Browse files
committed
Implement venv derivation
1 parent 9e6ffba commit 3a3abb9

File tree

4 files changed

+195
-62
lines changed

4 files changed

+195
-62
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,6 @@ jobs:
6969
activate-environment: true
7070
enable-cache: true
7171

72-
- name: Set Python DLL path and PYTHONHOME (non Windows)
73-
if: ${{ matrix.os.category != 'windows' }}
74-
run: |
75-
echo PYTHONNET_PYDLL=$(uv run find_libpython) >> $GITHUB_ENV
76-
77-
- name: Set Python DLL path and PYTHONHOME (Windows)
78-
if: ${{ matrix.os.category == 'windows' }}
79-
run: |
80-
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(uv run find_libpython)"
81-
8272
- name: Embedding tests
8373
run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/
8474
env:

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: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
else if (settings.TryGetValue("version_info", out versionStr))
91+
{
92+
_ = Version.TryParse(versionStr, out Version versionObj);
93+
env.Version = versionObj;
94+
}
95+
96+
env.LibPython = FindLibPython(env.Home, env.Version);
97+
98+
return env;
99+
}
100+
101+
private static Dictionary<string, string> TryParse(string venvCfg)
102+
{
103+
var settings = new Dictionary<string, string>();
104+
105+
string[] lines = File.ReadAllLines(venvCfg);
106+
107+
// The actually used format is really primitive: "<key> = <value>"
108+
foreach (string line in lines)
109+
{
110+
var split = line.Split(new[] { '=' }, 2);
111+
112+
if (split.Length != 2)
113+
continue;
114+
115+
settings[split[0].Trim()] = split[1].Trim();
116+
}
117+
118+
return settings;
119+
}
120+
121+
private static string? FindLibPython(string home, Version? maybeVersion)
122+
{
123+
// TODO: Check whether there is a .dll/.so/.dylib next to the executable
124+
125+
if (maybeVersion is Version version)
126+
{
127+
return FindLibPythonInHome(home, version);
128+
}
129+
130+
return null;
131+
}
132+
133+
private static string? FindLibPythonInHome(string home, Version version)
134+
{
135+
var libPythonName = GetDefaultDllName(version);
136+
137+
List<string> pathsToCheck = new();
138+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
139+
{
140+
var arch = RuntimeInformation.ProcessArchitecture;
141+
if (arch == Architecture.X64 || arch == Architecture.Arm64)
142+
{
143+
// multilib systems
144+
pathsToCheck.Add("../lib64");
145+
}
146+
pathsToCheck.Add("../lib");
147+
}
148+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
149+
{
150+
pathsToCheck.Add(".");
151+
}
152+
else
153+
{
154+
pathsToCheck.Add("../lib");
155+
}
156+
157+
return pathsToCheck
158+
.Select(path => Path.Combine(home, path, libPythonName))
159+
.FirstOrDefault(File.Exists);
160+
}
161+
162+
private static string ProgramNameFromPath(string path)
163+
{
164+
if (Runtime.IsWindows)
165+
{
166+
return Path.Combine(path, "Scripts", "python.exe");
167+
}
168+
else
169+
{
170+
return Path.Combine(path, "bin", "python");
171+
}
172+
}
173+
174+
internal static string GetDefaultDllName(Version version)
175+
{
176+
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
177+
178+
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
179+
? Invariant($"{version.Major}{version.Minor}")
180+
: Invariant($"{version.Major}.{version.Minor}");
181+
182+
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
183+
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
184+
: ".so";
185+
186+
return prefix + "python" + suffix + ext;
187+
}
188+
}

0 commit comments

Comments
 (0)