Skip to content

Commit 3d9a724

Browse files
committed
Fix Python executable detection for embedded Python
- sys.executable returns beam.smp when embedded - Use sys.prefix to find actual Python binary - Search for pythonX.Y, python3, python in prefix/bin
1 parent 26733b1 commit 3d9a724

2 files changed

Lines changed: 74 additions & 18 deletions

File tree

src/py.erl

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -777,21 +777,55 @@ venv_exists(Path) ->
777777
-spec create_venv(string(), list()) -> ok | {error, term()}.
778778
create_venv(Path, Opts) ->
779779
Installer = detect_installer(Opts),
780-
Python = proplists:get_value(python, Opts, "python3"),
780+
Python = case proplists:get_value(python, Opts, undefined) of
781+
undefined -> get_python_executable();
782+
P -> P
783+
end,
781784
Cmd = case Installer of
782785
uv ->
783-
%% uv venv is faster
784-
case proplists:get_value(python, Opts, undefined) of
785-
undefined ->
786-
io_lib:format("uv venv ~s", [quote(Path)]);
787-
PyVer ->
788-
io_lib:format("uv venv --python ~s ~s", [quote(PyVer), quote(Path)])
789-
end;
786+
%% uv venv is faster, use --python to match the running interpreter
787+
io_lib:format("uv venv --python ~s ~s", [quote(Python), quote(Path)]);
790788
pip ->
791789
io_lib:format("~s -m venv ~s", [quote(Python), quote(Path)])
792790
end,
793791
run_cmd(lists:flatten(Cmd)).
794792

793+
%% @private Get the Python executable path
794+
%% When embedded, sys.executable returns the embedding app (beam.smp)
795+
%% so we reconstruct the path from sys.prefix and version info
796+
-spec get_python_executable() -> string().
797+
get_python_executable() ->
798+
Code = <<"
799+
import sys, os
800+
# When embedded, sys.executable points to the embedding app
801+
# Use sys.prefix to find the actual Python installation
802+
if sys.platform == 'win32':
803+
python = os.path.join(sys.prefix, 'python.exe')
804+
else:
805+
ver = f'python{sys.version_info.major}.{sys.version_info.minor}'
806+
# Try common locations
807+
for path in [
808+
os.path.join(sys.prefix, 'bin', ver),
809+
os.path.join(sys.prefix, 'bin', 'python3'),
810+
os.path.join(sys.prefix, 'bin', 'python'),
811+
sys.executable # fallback
812+
]:
813+
if os.path.isfile(path) and os.access(path, os.X_OK):
814+
python = path
815+
break
816+
else:
817+
python = 'python3'
818+
python
819+
">>,
820+
case exec(Code) of
821+
ok ->
822+
case eval(<<"python">>) of
823+
{ok, Path} when is_binary(Path) -> binary_to_list(Path);
824+
_ -> "python3"
825+
end;
826+
_ -> "python3"
827+
end.
828+
795829
%% @private Install dependencies from requirements file
796830
-spec install_deps(string(), string(), list()) -> ok | {error, term()}.
797831
install_deps(Path, RequirementsFile, Opts) ->

test/py_venv_SUITE.erl

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,36 @@ end_per_suite(_Config) ->
6060
ok.
6161

6262
init_per_group(_Group, Config) ->
63-
Config.
63+
%% Get Python executable path from the running interpreter
64+
%% Note: sys.executable returns beam.smp when embedded, so we find the actual Python
65+
Code = <<"
66+
import sys, os
67+
ver = f'python{sys.version_info.major}.{sys.version_info.minor}'
68+
for path in [
69+
os.path.join(sys.prefix, 'bin', ver),
70+
os.path.join(sys.prefix, 'bin', 'python3'),
71+
os.path.join(sys.prefix, 'bin', 'python'),
72+
]:
73+
if os.path.isfile(path) and os.access(path, os.X_OK):
74+
_python_path = path
75+
break
76+
else:
77+
_python_path = 'python3'
78+
">>,
79+
ok = py:exec(Code),
80+
{ok, PythonPath} = py:eval(<<"_python_path">>),
81+
[{python_path, binary_to_list(PythonPath)} | Config].
6482

6583
end_per_group(_Group, _Config) ->
6684
ok.
6785

86+
%% @private Create venv using the Python from config
87+
create_test_venv(VenvPath, Config) ->
88+
PythonPath = ?config(python_path, Config),
89+
Cmd = PythonPath ++ " -m venv " ++ VenvPath,
90+
_ = os:cmd(Cmd),
91+
ok.
92+
6893
init_per_testcase(_TestCase, Config) ->
6994
%% Create unique temp directory for each test
7095
TempDir = filename:join(["/tmp", "py_venv_test_" ++ integer_to_list(erlang:unique_integer([positive]))]),
@@ -174,9 +199,8 @@ test_activate_venv(Config) ->
174199
TempDir = ?config(temp_dir, Config),
175200
VenvPath = filename:join(TempDir, "venv"),
176201

177-
%% Create venv manually
178-
Cmd = "python3 -m venv " ++ VenvPath,
179-
_ = os:cmd(Cmd),
202+
%% Create venv manually using the same Python we're linked against
203+
ok = create_test_venv(VenvPath, Config),
180204

181205
%% Activate it
182206
ok = py:activate_venv(VenvPath),
@@ -192,9 +216,8 @@ test_deactivate_venv(Config) ->
192216
TempDir = ?config(temp_dir, Config),
193217
VenvPath = filename:join(TempDir, "venv"),
194218

195-
%% Create and activate venv
196-
Cmd = "python3 -m venv " ++ VenvPath,
197-
_ = os:cmd(Cmd),
219+
%% Create and activate venv using the same Python we're linked against
220+
ok = create_test_venv(VenvPath, Config),
198221
ok = py:activate_venv(VenvPath),
199222

200223
%% Verify active
@@ -217,9 +240,8 @@ test_venv_info(Config) ->
217240
{ok, Info1} = py:venv_info(),
218241
false = maps:get(<<"active">>, Info1),
219242

220-
%% Create and activate
221-
Cmd = "python3 -m venv " ++ VenvPath,
222-
_ = os:cmd(Cmd),
243+
%% Create and activate using the same Python we're linked against
244+
ok = create_test_venv(VenvPath, Config),
223245
ok = py:activate_venv(VenvPath),
224246

225247
%% After activation, should have all info

0 commit comments

Comments
 (0)