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 <array>
9#include <cstdio>
10#include <filesystem>
11#include <memory>
12#include <optional>
13#include <string>
14#include <string_view>
15#include <vector>
16
17#include "BaseLib/Error.h"
18#include "BaseLib/Logging.h"
22
31
32namespace ApplicationsLib
33{
34pybind11::scoped_interpreter setupEmbeddedPython()
35{
36 // Allows ogs to be interrupted by SIGINT, which otherwise is handled by
37 // python. See
38 // https://docs.python.org/3/c-api/exceptions.html#c.PyErr_CheckSignals and
39 // https://pybind11.readthedocs.io/en/stable/faq.html#how-can-i-properly-handle-ctrl-c-in-long-running-functions
40 constexpr bool init_signal_handlers = false;
41 return pybind11::scoped_interpreter{init_signal_handlers};
42}
43
44// Rest of the file handles venv compatibility checks and sys.path handling.
45namespace
46{
47#ifdef _WIN32
49struct PipeCloser
50{
51 void operator()(FILE* f) const { _pclose(f); }
52};
53
55std::optional<std::string> executeCommand(std::string_view command)
56{
57 std::array<char, 256> buffer;
58 std::string result;
59 std::unique_ptr<FILE, PipeCloser> pipe(_popen(command.data(), "r"));
60
61 if (!pipe)
62 {
63 DBUG("Failed to execute command: {}", command);
64 return std::nullopt;
65 }
66
67 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
68 {
69 result += buffer.data();
70 }
71
72 return result;
73}
74
76std::optional<std::pair<int, int>> getPythonVersionFromVenv(
77 std::filesystem::path const& venv_path)
78{
79 namespace fs = std::filesystem;
80
81 auto const python_exe = venv_path / "Scripts" / "python.exe";
82
83 if (!fs::exists(python_exe))
84 {
85 DBUG("Python executable not found at: {}", python_exe.string());
86 return std::nullopt;
87 }
88
89 std::string const command = "\"" + python_exe.string() + "\" --version";
90 auto const output = executeCommand(command);
91
92 if (!output.has_value())
93 {
94 DBUG("Failed to get Python version from: {}", python_exe.string());
95 return std::nullopt;
96 }
97
98 // Parse output like "Python 3.11.5"
99 std::string_view const out_view(output.value());
100 constexpr std::string_view prefix = "Python ";
101 if (!out_view.starts_with(prefix))
102 {
103 DBUG("Unexpected Python version output: {}", output.value());
104 return std::nullopt;
105 }
106
107 std::string_view const version_part = out_view.substr(prefix.size());
108 int major = 0;
109 int minor = 0;
110 if (std::sscanf(version_part.data(), "%d.%d", &major, &minor) != 2)
111 {
112 DBUG("Failed to parse Python version from: {}", output.value());
113 return std::nullopt;
114 }
115
116 return std::pair{major, minor};
117}
118#endif // _WIN32
119
120std::vector<std::filesystem::path> findAlternativeSitePackagesPaths(
121 std::filesystem::path const& venv_path)
122{
123 namespace fs = std::filesystem;
124
125 std::vector<fs::path> alternatives;
126 fs::path const lib_path = venv_path / "lib";
127
128 if (!fs::exists(lib_path) || !fs::is_directory(lib_path))
129 {
130 // Should not happen, i.e. if venv directory is not valid
131 return {};
132 }
133
134 for (auto const& entry : fs::directory_iterator(lib_path))
135 {
136 if (!entry.is_directory())
137 {
138 continue;
139 }
140
141 std::string const dirname = entry.path().filename().string();
142 if (!dirname.starts_with("python"))
143 {
144 continue;
145 }
146
147 fs::path const candidate = entry.path() / "site-packages";
148 if (fs::exists(candidate) && fs::is_directory(candidate))
149 {
150 alternatives.push_back(candidate);
151 }
152 }
153
154 return alternatives;
155}
156
158std::filesystem::path findSitePackagesPath(
159 std::filesystem::path const& venv_path, int const emb_major,
160 int const emb_minor)
161{
162 namespace fs = std::filesystem;
163
164#ifdef _WIN32
165 // On Windows only: compare embedded python interpreter version with the
166 // version of the python executable in the virtual environment.
167 auto const venv_version = getPythonVersionFromVenv(venv_path);
168 if (!venv_version.has_value())
169 {
170 OGS_FATAL(
171 "Failed to determine Python version from virtual environment at "
172 "'{}'.",
173 venv_path.string());
174 }
175
176 if (venv_version->first != emb_major || venv_version->second != emb_minor)
177 {
178 OGS_FATAL(
179 "Python version mismatch: embedded interpreter is {}.{}, but "
180 "virtual environment at '{}' uses {}.{}.",
181 emb_major, emb_minor, venv_path.string(), venv_version->first,
182 venv_version->second);
183 }
184
185 fs::path const site_packages = venv_path / "Lib" / "site-packages";
186#else
187 // On Linux / macOS: Construct path to site-packages directory where the
188 // Python version is embedded in the path. Then check for compatibility.
189 // E.g.: .venv/lib/python3.14/site-packages.
190 // Executing the virtual environment's Python interpreter may not possible
191 // when executing Python BCs from within a container environment, i.e.
192 // this would execute Python from the host inside the container.
193 fs::path const site_packages = venv_path / "lib" /
194 ("python" + std::to_string(emb_major) + "." +
195 std::to_string(emb_minor)) /
196 "site-packages";
197#endif // _WIN32
198
199 if (!fs::exists(site_packages))
200 {
201#ifndef _WIN32
202 // If correct site-packages directory is not found, check for
203 // directories for other Python versions, indicating a Python version
204 // mismatch. Possible on Linux / macOS only.
205 auto const alternatives = findAlternativeSitePackagesPaths(venv_path);
206 if (!alternatives.empty())
207 {
208 std::string alternative_paths;
209 for (std::size_t i = 0; i < alternatives.size(); ++i)
210 {
211 if (i > 0)
212 {
213 alternative_paths += ", ";
214 }
215 alternative_paths += "'" + alternatives[i].string() + "'";
216 }
217
218 WARN(
219 "Expected site-packages directory '{}' was not found. "
220 "Found other site-packages directory/directories: {}. This "
221 "may indicate a Python version mismatch between the embedded "
222 "interpreter {}.{} and the virtual environment.",
223 site_packages.string(), alternative_paths, emb_major,
224 emb_minor);
225 }
226#endif // _WIN32
227 OGS_FATAL("site-packages directory not found at '{}'",
228 site_packages.string());
229 }
230
231 return site_packages;
232}
233} // anonymous namespace
234
236{
237 namespace py = pybind11;
238 namespace fs = std::filesystem;
239
240 // Get embedded Python version
241 py::object const version_info =
242 py::module_::import("sys").attr("version_info");
243 int const emb_major = version_info.attr("major").cast<int>();
244 int const emb_minor = version_info.attr("minor").cast<int>();
245
246 // Check for virtual environment
247 char const* const venv = std::getenv("VIRTUAL_ENV");
248 if (venv == nullptr)
249 {
250 DBUG("No virtual environment detected (VIRTUAL_ENV not set).");
251 return;
252 }
253
254 fs::path const venv_path(venv);
255 DBUG("Virtual environment detected at: {}", venv_path.string());
256
257 // Find and validate site-packages path for the embedded interpreter
258 // version.
259 fs::path const site_packages =
260 findSitePackagesPath(venv_path, emb_major, emb_minor);
261 INFO("Using virtual environment site-packages: {}", site_packages.string());
262
263 // Add to sys.path (insert at beginning for highest priority)
264 py::list sys_path = py::module_::import("sys").attr("path");
265 sys_path.insert(0, py::str(site_packages.string()));
266}
267
268} // 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
void WARN(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:34
std::vector< std::filesystem::path > findAlternativeSitePackagesPaths(std::filesystem::path const &venv_path)
std::filesystem::path findSitePackagesPath(std::filesystem::path const &venv_path, int const emb_major, int const emb_minor)
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)