Skip to content

Commit f9dfafe

Browse files
Update init so that sys.executable points to python (#4)
1 parent 8662406 commit f9dfafe

File tree

6 files changed

+87
-53
lines changed

6 files changed

+87
-53
lines changed

c_src/pythonx/python.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ DEF_SYMBOL(Py_IsFalse)
7777
DEF_SYMBOL(Py_IsNone)
7878
DEF_SYMBOL(Py_IsTrue)
7979
DEF_SYMBOL(Py_SetPythonHome)
80+
DEF_SYMBOL(Py_SetProgramName)
8081

8182
dl::LibraryHandle python_library;
8283

@@ -150,6 +151,7 @@ void load_python_library(std::string path) {
150151
LOAD_SYMBOL(python_library, Py_IsNone)
151152
LOAD_SYMBOL(python_library, Py_IsTrue)
152153
LOAD_SYMBOL(python_library, Py_SetPythonHome)
154+
LOAD_SYMBOL(python_library, Py_SetProgramName)
153155
}
154156

155157
void unload_python_library() {

c_src/pythonx/python.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ extern int (*Py_IsFalse)(PyObjectPtr);
131131
extern int (*Py_IsNone)(PyObjectPtr);
132132
extern int (*Py_IsTrue)(PyObjectPtr);
133133
extern void (*Py_SetPythonHome)(const wchar_t *);
134+
extern void (*Py_SetProgramName)(const wchar_t *);
134135

135136
// Opens Python dynamic library at the given path and looks up all
136137
// relevant symbols.

c_src/pythonx/pythonx.cpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ using namespace python;
2323
std::mutex init_mutex;
2424
bool is_initialized = false;
2525
std::wstring python_home_path_w;
26+
std::wstring python_executable_path_w;
2627
std::map<std::string, std::tuple<PyObjectPtr, PyObjectPtr>> compilation_cache;
2728
std::mutex compilation_cache_mutex;
2829

@@ -225,6 +226,7 @@ ERL_NIF_TERM py_object_to_binary_term(ErlNifEnv *env, PyObjectPtr py_object) {
225226

226227
fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
227228
ErlNifBinary python_home_path,
229+
ErlNifBinary python_executable_path,
228230
std::vector<ErlNifBinary> sys_paths) {
229231
auto init_guard = std::lock_guard<std::mutex>(init_mutex);
230232

@@ -240,24 +242,36 @@ fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
240242
python_home_path_w = std::wstring(
241243
python_home_path.data, python_home_path.data + python_home_path.size);
242244

243-
// As part of the initialization, sys.path is set. It is important
245+
python_executable_path_w =
246+
std::wstring(python_executable_path.data,
247+
python_executable_path.data + python_executable_path.size);
248+
249+
// As part of the initialization, sys.path gets set. It is important
244250
// that it gets set correctly, so that the built-in modules can be
245251
// found, otherwise the initialization fails. This logic is internal
246252
// to Python, but we can configure base paths used to infer sys.path.
247-
// The Limited API exposes Py_SetPythonHome and Py_SetProgramName.
248-
// Technically we could use either of them, while Py_SetProgramName
249-
// has the advantage that, when set to the executable inside venv,
250-
// it results in the packages directory being added to sys.path
251-
// automatically. However, when testing Py_SetProgramName did not
252-
// work as expected in Python 3.10 on Windows. For this reason we
253-
// use Py_SetPythonHome, which seems more reliable, and add other
254-
// paths to sys.path manually.
253+
// The Limited API exposes Py_SetPythonHome and Py_SetProgramName and
254+
// it appears that setting either of them alone should be sufficient.
255+
//
256+
// Py_SetProgramName has the advantage that, when set to the executable
257+
// inside venv, it results in the packages directory being added to
258+
// sys.path automatically, however, when tested, this did not work
259+
// as expected in Python 3.10 on Windows. For this reason we prefer
260+
// to use Py_SetPythonHome and add other paths to sys.path manually.
261+
//
262+
// Even then, we still want to set Py_SetProgramName to a Python
263+
// executable, otherwise `sys.executable` is going to point to the
264+
// BEAM executable (`argv[0]`), which can be problematic.
265+
//
266+
// In the end, the most reliable combination seems to be to set both,
267+
// and also add the extra sys.path manually.
255268
//
256269
// Note that Python home is the directory with lib/ child directory
257270
// containing the built-in Python modules [1].
258271
//
259272
// [1]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
260273
Py_SetPythonHome(python_home_path_w.c_str());
274+
Py_SetProgramName(python_executable_path_w.c_str());
261275

262276
Py_InitializeEx(0);
263277

lib/pythonx.ex

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,6 @@ defmodule Pythonx do
1212

1313
@type encoder :: (term(), encoder() -> Object.t())
1414

15-
@doc """
16-
Initializes the Python interpreter.
17-
18-
> #### Reproducability {: .info}
19-
>
20-
> This function can be called to use a custom Python installation,
21-
> however in most cases it is more convenient to call `uv_init/2`,
22-
> which installs Python and dependencies, and then automatically
23-
> initializes the interpreter using the correct paths.
24-
25-
The `python_dl_path` argument is the Python dynamically linked
26-
library file. The usual file name is `libpython3.x.so` (Linux),
27-
`libpython3.x.dylib` (macOS), `python3x.dll` (Windows).
28-
29-
The `python_home_path` is the Python home directory, where the Python
30-
built-in modules reside. Specifically, the modules should be located
31-
in `{python_home_path}/lib/pythonx.y` (Linux and macOS) or
32-
`{python_home_path}/Lib` (Windows).
33-
34-
## Options
35-
36-
* `:sys_paths` - directories to be added to the module search path
37-
(`sys.path`). Defaults to `[]`.
38-
39-
"""
40-
@spec init(String.t(), String.t(), keyword()) :: :ok
41-
def init(python_dl_path, python_home_path, opts \\ [])
42-
when is_binary(python_dl_path) and is_binary(python_home_path) and is_list(opts) do
43-
opts = Keyword.validate!(opts, sys_paths: [])
44-
45-
if not File.exists?(python_dl_path) do
46-
raise ArgumentError, "the given dynamic library file does not exist: #{python_dl_path}"
47-
end
48-
49-
if not File.dir?(python_home_path) do
50-
raise ArgumentError, "the given python home directory does not exist: #{python_home_path}"
51-
end
52-
53-
Pythonx.NIF.init(python_dl_path, python_home_path, opts[:sys_paths])
54-
end
55-
5615
@doc ~S'''
5716
Installs Python and dependencies using [uv](https://docs.astral.sh/uv)
5817
package manager and initializes the interpreter.
@@ -99,6 +58,53 @@ defmodule Pythonx do
9958
Pythonx.Uv.init(pyproject_toml, false)
10059
end
10160

61+
# Initializes the Python interpreter.
62+
#
63+
# > #### Reproducability {: .info}
64+
# >
65+
# > This function can be called to use a custom Python installation,
66+
# > however in most cases it is more convenient to call `uv_init/2`,
67+
# > which installs Python and dependencies, and then automatically
68+
# > initializes the interpreter using the correct paths.
69+
#
70+
# `python_dl_path` is the Python dynamically linked library file.
71+
# The usual file name is `libpython3.x.so` (Linux), `libpython3.x.dylib`
72+
# (macOS), `python3x.dll` (Windows).
73+
#
74+
# `python_home_path` is the Python home directory, where the Python
75+
# built-in modules reside. Specifically, the modules should be
76+
# located in `{python_home_path}/lib/pythonx.y` (Linux and macOS)
77+
# or `{python_home_path}/Lib` (Windows).
78+
#
79+
# `python_executable_path` is the Python executable file.
80+
#
81+
# ## Options
82+
#
83+
# * `:sys_paths` - directories to be added to the module search path
84+
# (`sys.path`). Defaults to `[]`.
85+
#
86+
@doc false
87+
@spec init(String.t(), String.t(), keyword()) :: :ok
88+
def init(python_dl_path, python_home_path, python_executable_path, opts \\ [])
89+
when is_binary(python_dl_path) and is_binary(python_home_path)
90+
when is_binary(python_executable_path) and is_list(opts) do
91+
opts = Keyword.validate!(opts, sys_paths: [])
92+
93+
if not File.exists?(python_dl_path) do
94+
raise ArgumentError, "the given dynamic library file does not exist: #{python_dl_path}"
95+
end
96+
97+
if not File.dir?(python_home_path) do
98+
raise ArgumentError, "the given python home directory does not exist: #{python_home_path}"
99+
end
100+
101+
if not File.exists?(python_home_path) do
102+
raise ArgumentError, "the given python executable does not exist: #{python_executable_path}"
103+
end
104+
105+
Pythonx.NIF.init(python_dl_path, python_home_path, python_executable_path, opts[:sys_paths])
106+
end
107+
102108
@doc ~S'''
103109
Evaluates the Python `code`.
104110

lib/pythonx/nif.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule Pythonx.NIF do
1212
end
1313
end
1414

15-
def init(_python_dl_path, _python_home_path, _sys_paths), do: err!()
15+
def init(_python_dl_path, _python_home_path, _python_executable_path, _sys_paths), do: err!()
1616
def terminate(), do: err!()
1717
def janitor_decref(_ptr), do: err!()
1818
def none_new(), do: err!()

lib/pythonx/uv.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,19 @@ defmodule Pythonx.Uv do
106106

107107
python_home_path = make_windows_slashes(root_dir)
108108

109+
python_executable_path =
110+
abs_executable_dir
111+
|> Path.join("python.exe")
112+
|> make_windows_slashes()
113+
109114
venv_packages_path =
110115
project_dir
111116
|> Path.join(".venv/Lib/site-packages")
112117
|> make_windows_slashes()
113118

114-
Pythonx.init(python_dl_path, python_home_path, sys_paths: [venv_packages_path])
119+
Pythonx.init(python_dl_path, python_home_path, python_executable_path,
120+
sys_paths: [venv_packages_path]
121+
)
115122

116123
{:unix, osname} ->
117124
dl_extension =
@@ -128,12 +135,16 @@ defmodule Pythonx.Uv do
128135

129136
python_home_path = root_dir
130137

138+
python_executable_path = Path.join(abs_executable_dir, "python")
139+
131140
venv_packages_path =
132141
project_dir
133142
|> Path.join(".venv/lib/python3*/site-packages")
134143
|> wildcard_one!()
135144

136-
Pythonx.init(python_dl_path, python_home_path, sys_paths: [venv_packages_path])
145+
Pythonx.init(python_dl_path, python_home_path, python_executable_path,
146+
sys_paths: [venv_packages_path]
147+
)
137148
end
138149
end
139150

0 commit comments

Comments
 (0)