OGS
ogs_embedded_python.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
2// SPDX-License-Identifier: BSD-3-Clause
3
5
6#include <pybind11/embed.h>
7
8#include <charconv>
9#include <cstdio>
10#include <filesystem>
11#include <optional>
12#include <string>
13#include <string_view>
14
15#include "BaseLib/Error.h"
16#include "BaseLib/Logging.h"
20
29
30namespace ApplicationsLib
31{
32pybind11::scoped_interpreter setupEmbeddedPython()
33{
34 // Allows ogs to be interrupted by SIGINT, which otherwise is handled by
35 // python. See
36 // https://docs.python.org/3/c-api/exceptions.html#c.PyErr_CheckSignals and
37 // https://pybind11.readthedocs.io/en/stable/faq.html#how-can-i-properly-handle-ctrl-c-in-long-running-functions
38 constexpr bool init_signal_handlers = false;
39 return pybind11::scoped_interpreter{init_signal_handlers};
40}
41
42namespace
43{
46{
47#ifdef _WIN32
48 void operator()(FILE* f) const { _pclose(f); }
49#else
50 void operator()(FILE* f) const { pclose(f); }
51#endif // _WIN32
52};
53
55std::optional<std::string> executeCommand(std::string_view command)
56{
57 std::array<char, 256> buffer;
58 std::string result;
59
60#ifdef _WIN32
61 std::unique_ptr<FILE, PipeCloser> pipe(_popen(command.data(), "r"));
62#else
63 std::unique_ptr<FILE, PipeCloser> pipe(popen(command.data(), "r"));
64#endif // _WIN32
65
66 if (!pipe)
67 {
68 DBUG("Failed to execute command: {}", command);
69 return std::nullopt;
70 }
71
72 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
73 {
74 result += buffer.data();
75 }
76
77 return result;
78}
79
81std::optional<std::pair<int, int>> getPythonVersionFromVenv(
82 std::filesystem::path const& venv_path)
83{
84 namespace fs = std::filesystem;
85
86 fs::path python_exe;
87#ifdef _WIN32
88 python_exe = venv_path / "Scripts" / "python.exe";
89#else
90 python_exe = venv_path / "bin" / "python";
91#endif // _WIN32
92
93 if (!fs::exists(python_exe))
94 {
95 DBUG("Python executable not found at: {}", python_exe.string());
96 return std::nullopt;
97 }
98
99 std::string const command = "\"" + python_exe.string() + "\" --version";
100 auto const output = executeCommand(command);
101
102 if (!output.has_value())
103 {
104 DBUG("Failed to get Python version from: {}", python_exe.string());
105 return std::nullopt;
106 }
107
108 // Parse output like "Python 3.11.5"
109 std::string_view const out_view(output.value());
110 constexpr std::string_view prefix = "Python ";
111 std::string_view version_part = out_view.substr(prefix.size());
112 int major = 0, minor = 0;
113
114 auto dot = version_part.find('.');
115 if (dot == std::string_view::npos)
116 {
117 DBUG("Failed to parse Python version from: {}", out_view);
118 return std::nullopt;
119 }
120
121 auto parse = [](std::string_view sv, int& value)
122 {
123 auto [ptr, ec] =
124 std::from_chars(sv.data(), sv.data() + sv.size(), value);
125 return ec == std::errc{} && ptr != sv.data();
126 };
127
128 if (!parse(version_part.substr(0, dot), major) ||
129 !parse(version_part.substr(dot + 1), minor))
130 {
131 DBUG("Failed to parse Python version from: {}", out_view);
132 return std::nullopt;
133 }
134
135 DBUG("Detected Python version {}.{} from venv", major, minor);
136 return std::make_pair(major, minor);
137}
138
140std::filesystem::path findSitePackagesPath(
141 std::filesystem::path const& venv_path)
142{
143 namespace fs = std::filesystem;
144
145#ifdef _WIN32
146 // On Windows: venv/Lib/site-packages
147 fs::path const site_packages = venv_path / "Lib" / "site-packages";
148#else
149 // On Unix: venv/lib/pythonX.Y/site-packages
150 auto const version = getPythonVersionFromVenv(venv_path);
151 if (!version.has_value())
152 {
153 OGS_FATAL(
154 "Failed to determine Python version of the virtual environment.");
155 }
156 fs::path const site_packages = venv_path / "lib" /
157 ("python" + std::to_string(version->first) +
158 "." + std::to_string(version->second)) /
159 "site-packages";
160#endif // _WIN32
161
162 if (!fs::exists(site_packages))
163 {
164 OGS_FATAL("site-packages directory not found at '{}'",
165 site_packages.string());
166 }
167
168 return site_packages;
169}
170} // anonymous namespace
171
173{
174 namespace py = pybind11;
175 namespace fs = std::filesystem;
176
177 // Get embedded Python version
178 py::object const version_info =
179 py::module_::import("sys").attr("version_info");
180 int const emb_major = version_info.attr("major").cast<int>();
181 int const emb_minor = version_info.attr("minor").cast<int>();
182
183 // Check for virtual environment
184 char const* const venv = std::getenv("VIRTUAL_ENV");
185 if (venv == nullptr)
186 {
187 DBUG("No virtual environment detected (VIRTUAL_ENV not set).");
188 return;
189 }
190
191 fs::path const venv_path(venv);
192 DBUG("Virtual environment detected at: {}", venv_path.string());
193
194 auto const venv_version = getPythonVersionFromVenv(venv_path);
195 if (!venv_version.has_value())
196 {
197 OGS_FATAL(
198 "Failed to determine Python version from virtual environment at "
199 "'{}'. "
200 "Please ensure the virtual environment is valid.",
201 venv_path.string());
202 }
203
204 int const venv_major = venv_version->first;
205 int const venv_minor = venv_version->second;
206
207 // Validate version match
208 if (venv_major != emb_major || venv_minor != emb_minor)
209 {
210 OGS_FATAL(
211 "Python version mismatch:\n"
212 " Embedded interpreter: {}.{}\n"
213 " Virtual environment: {}.{}\n"
214 "The virtual environment must use the same Python version as the "
215 "embedded interpreter.",
216 emb_major, emb_minor, venv_major, venv_minor);
217 }
218
219 // Find and validate site-packages path
220 fs::path const site_packages = findSitePackagesPath(venv_path);
221 INFO("Using virtual environment site-packages: {}", site_packages.string());
222
223 // Add to sys.path (insert at beginning for highest priority)
224 py::list sys_path = py::module_::import("sys").attr("path");
225 sys_path.insert(0, py::str(site_packages.string()));
226}
227
228} // namespace ApplicationsLib
#define OGS_FATAL(...)
Definition Error.h:19
void INFO(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:28
void DBUG(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:22
std::optional< std::string > executeCommand(std::string_view command)
Executes a command and captures its stdout output using popen.
std::optional< std::pair< int, int > > getPythonVersionFromVenv(std::filesystem::path const &venv_path)
Gets Python version by executing the venv's python executable.
std::filesystem::path findSitePackagesPath(std::filesystem::path const &venv_path)
Finds site-packages path in the virtual environment.
pybind11::scoped_interpreter setupEmbeddedPython()
void pythonBindSourceTerm(pybind11::module &m)
Creates Python bindings for the Python source term class.
void pythonBindBoundaryCondition(pybind11::module &m)
Creates Python bindings for the Python BC class.
void bheInflowpythonBindBoundaryCondition(pybind11::module &m)
Creates BHE Inflow Python bindings for the Python BC class.
PYBIND11_EMBEDDED_MODULE(OpenGeoSys, m)