ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PythonConfig.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "PythonConfig.h"
9 #include "Utilities.h"
10 
11 #include <QApplication>
12 #include <QDebug>
13 #include <QDir>
14 #include <QMessageBox>
15 #include <QProcess>
16 #include <QVector>
17 #include <QtGlobal>
18 
19 // Qt5/Qt6 Compatibility
20 #include <QtCompat.h>
21 
22 #if defined(USE_EMBEDDED_MODULES)
23 #if defined(Q_OS_WINDOWS)
24 static QString BundledSitePackagesPath()
25 {
26  return QDir::listSeparator() + QApplication::applicationDirPath() +
27  "/plugins/Python/Lib/site-packages";
28 }
29 #elif defined(Q_OS_MACOS)
30 static QString BundledSitePackagesPath()
31 {
32  return QDir::listSeparator() + QApplication::applicationDirPath() +
33  "/../Resources/python/lib/site-packages";
34 }
35 #else
36 static QString BundledSitePackagesPath()
37 {
38  return QDir::listSeparator() + QApplication::applicationDirPath() +
39  "/plugins/Python/lib/site-packages";
40 }
41 #endif
42 #endif
43 
44 //================================================================================
45 
47 {
48  QString str = qtCompatStringRefToString(versionStr);
49  auto parts = qtCompatSplitRefChar(str, '.');
50  if (parts.size() == 3)
51  {
52 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
53  versionMajor = parts[0].toString().toUInt();
54  versionMinor = parts[1].toString().toUInt();
55  versionPatch = parts[2].toString().toUInt();
56 #else
57  versionMajor = parts[0].toUInt();
58  versionMinor = parts[1].toUInt();
59  versionPatch = parts[2].toUInt();
60 #endif
61  }
62 }
63 
65 {
67 }
68 
69 bool Version::operator==(const Version &other) const
70 {
71  return versionMajor == other.versionMajor && versionMinor == other.versionMinor &&
72  versionPatch == other.versionPatch;
73 }
74 
75 static Version GetPythonExeVersion(QProcess &pythonProcess)
76 {
77  pythonProcess.setArguments({"--version"});
78  pythonProcess.start(QIODevice::ReadOnly);
79  pythonProcess.waitForFinished();
80 
81  const QString versionStr =
82  qtCompatCodecForName("utf-8")->toUnicode(pythonProcess.readAllStandardOutput());
83 
84  auto splits = qtCompatSplitRefChar(versionStr, ' ');
85 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
86  if (splits.size() == 2 && splits[0].toString().contains("Python"))
87  {
88  return Version(qtCompatStringRef(splits[1].toString()));
89 #else
90  if (splits.size() == 2 && splits[0].contains("Python"))
91  {
92  return Version(qtCompatStringRef(splits[1].toString()));
93 #endif
94  }
95  return Version{};
96 }
97 //================================================================================
98 
99 struct PyVenvCfg
100 {
101  PyVenvCfg() = default;
102 
103  static PyVenvCfg FromFile(const QString &path);
104 
105  QString home{};
108 };
109 
111 {
112  PyVenvCfg cfg{};
113 
114  QFile cfgFile(path);
115  if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text))
116  {
117  while (!cfgFile.atEnd())
118  {
119  QString line = cfgFile.readLine();
120  QStringList v = line.split("=");
121 
122  if (v.size() == 2)
123  {
124  QString name = v[0].simplified();
125  QString value = v[1].simplified();
126 
127  if (name == "home")
128  {
129  cfg.home = value;
130  }
131  else if (name == "include-system-site-packages")
132  {
133  cfg.includeSystemSitesPackages = (value == "true");
134  }
135  else if (name == "version")
136  {
137  cfg.version = Version(qtCompatStringRef(value));
138  }
139  }
140  }
141  }
142 
143  return cfg;
144 }
145 
146 //================================================================================
147 
149 {
150  return m_pythonHome != nullptr && m_pythonPath != nullptr;
151 }
152 
153 const wchar_t *PythonConfigPaths::pythonHome() const
154 {
155  return m_pythonHome.get();
156 }
157 
158 const wchar_t *PythonConfigPaths::pythonPath() const
159 {
160  return m_pythonPath.get();
161 }
162 
163 //================================================================================
164 
165 static QString PathToPythonExecutableInEnv(PythonConfig::Type envType, const QString &envRoot)
166 {
167 #if defined(Q_OS_WINDOWS)
168  switch (envType)
169  {
171  return envRoot + "/python.exe";
173  return envRoot + "/Scripts/python.exe";
175  return envRoot + "/python.exe";
177  return "python.exe";
178  }
179 #else
180  switch (envType)
181  {
185  return envRoot + "/bin/python";
187  return "python";
188  }
189 #endif
190  return {};
191 }
192 
194 {
195 #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
196  m_type = Type::System;
197 #else
198  m_type = Type::Bundled;
199 #endif
200 }
201 
203 {
204 #if defined(Q_OS_MACOS)
205  const QString pythonEnvDirPath(QApplication::applicationDirPath() + "/../Resources/python");
206 #else
207  const QString pythonEnvDirPath(QApplication::applicationDirPath() + "/plugins/Python");
208 #endif
209  initFromLocation(pythonEnvDirPath);
210 }
211 
212 void PythonConfig::initFromLocation(const QString &prefix)
213 {
214  QDir envRoot(prefix);
215 
216  if (!envRoot.exists())
217  {
218  m_pythonHome = QString();
219  m_pythonPath = QString();
220  m_type = Type::Bundled;
221  return;
222  }
223 
224  if (envRoot.exists("pyvenv.cfg"))
225  {
226  QString pythonExePath = PathToPythonExecutableInEnv(Type::Venv, prefix);
227  initFromPythonExecutable(pythonExePath);
228  if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty())
229  {
230  qDebug() << "Failed to get paths info from python executable at (venv)"
231  << pythonExePath;
232  initVenv(envRoot.path());
233  }
234  else
235  {
236  m_type = Type::Venv;
237  }
238  }
239  else if (envRoot.exists("conda-meta"))
240  {
241  QString pythonExePath = PathToPythonExecutableInEnv(Type::Conda, prefix);
242  initFromPythonExecutable(pythonExePath);
243  if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty())
244  {
245  qDebug() << "Failed to get paths info from python executable at (conda)"
246  << pythonExePath;
247  initCondaEnv(envRoot.path());
248  }
249  else
250  {
251  m_type = Type::Conda;
252  }
253  }
254  else
255 #if defined(Q_OS_WIN32) || defined(Q_OS_MACOS)
256  {
257  QString pythonExePath = PathToPythonExecutableInEnv(Type::Bundled, prefix);
258  initFromPythonExecutable(pythonExePath);
259  if (m_pythonHome.isEmpty() && m_pythonPath.isEmpty())
260  {
261  qDebug() << "Failed to get paths info from python executable at (bundled)"
262  << pythonExePath;
263  initVenv(envRoot.path());
264  }
265  else
266  {
267  m_type = Type::Bundled;
268  }
269  }
270 #else
271  {
272  m_pythonHome = envRoot.path();
273  m_pythonPath = QString("%1/DLLs;%1/lib;%1/Lib;%1/Lib/site-packages;").arg(m_pythonHome);
274  m_type = Type::Bundled;
275 
276 #if defined(USE_EMBEDDED_MODULES)
277  m_pythonPath.append(BundledSitePackagesPath());
278 #endif
279  }
280 #endif
281 }
282 
283 void PythonConfig::initCondaEnv(const QString &condaPrefix)
284 {
285  m_type = Type::Conda;
286  m_pythonHome = condaPrefix;
287  m_pythonPath = QString("%1/DLLs;%1/lib;%1/Lib;%1/Lib/site-packages;").arg(condaPrefix);
288 
289 #if defined(USE_EMBEDDED_MODULES)
290  m_pythonPath.append(BundledSitePackagesPath());
291 #endif
292 }
293 
294 void PythonConfig::initVenv(const QString &venvPrefix)
295 {
296  PyVenvCfg cfg = PyVenvCfg::FromFile(QString("%1/pyvenv.cfg").arg(venvPrefix));
297 
298  m_type = Type::Venv;
299  m_pythonHome = venvPrefix;
300  m_pythonPath = QString("%1/Lib;%1/Lib/site-packages;%3/DLLs;%3/lib;").arg(venvPrefix, cfg.home);
302  {
303  m_pythonPath.append(QString("%1/Lib/site-packages;").arg(cfg.home));
304  }
305 
306 #if defined(USE_EMBEDDED_MODULES)
307  m_pythonPath.append(BundledSitePackagesPath());
308 #endif
309 }
310 
311 void PythonConfig::preparePythonProcess(QProcess &pythonProcess) const
312 {
313  const QString pythonExePath = PathToPythonExecutableInEnv(type(), m_pythonHome);
314  pythonProcess.setProgram(pythonExePath);
315 
316  // Conda env have SSL related libraries stored in a part that is not
317  // in the path of the python exe, we have to add it ourselves.
318  if (m_type == Type::Conda)
319  {
320 #if defined(Q_OS_WINDOWS)
321  const QString additionalPath = QString("%1/Library/bin").arg(m_pythonHome);
322 #else
323  const QString additionalPath = QString("%1/lib/bin").arg(m_pythonHome);
324 #endif
325 
326  QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
327  QString path = env.value("PATH").append(QDir::listSeparator()).append(additionalPath);
328  env.insert("PATH", path);
329  pythonProcess.setProcessEnvironment(env);
330  }
331 }
332 
334 {
335  PythonConfigPaths paths;
336  paths.m_pythonHome.reset(QStringToWcharArray(m_pythonHome));
337  paths.m_pythonPath.reset(QStringToWcharArray(m_pythonPath));
338  return paths;
339 }
340 
342 {
343  QProcess pythonProcess;
344  preparePythonProcess(pythonProcess);
345  return GetPythonExeVersion(pythonProcess);
346 }
347 
348 bool PythonConfig::validateAndDisplayErrors(QWidget *parent) const
349 {
350  Version envVersion = getVersion();
351  if (envVersion.isNull())
352  {
353  // This hints that the selected directory is likely not valid.
354  QMessageBox::warning(
355  parent,
356  "Invalid Python Environment",
357  "The selected directory does not seems to be a valid python environment");
358  return false;
359  }
360 
361  if (!envVersion.isCompatibleWithCompiledVersion())
362  {
363  QMessageBox::warning(
364  parent,
365  "Incompatible Python Environment",
366  QString("The selected directory does not contain a Python Environment that is "
367  "compatible. Expected a python version like %1.%2.x, selected environment "
368  "has version %3.%4.%5")
369  .arg(QString::number(PythonVersion.versionMajor),
370  QString::number(PythonVersion.versionMinor),
371  QString::number(envVersion.versionMajor),
372  QString::number(envVersion.versionMinor),
373  QString::number(envVersion.versionPatch)));
374  return false;
375  }
376 
377  return true;
378 }
379 
381 {
382  return qEnvironmentVariableIsSet("CONDA_PREFIX") || qEnvironmentVariableIsSet("VIRTUAL_ENV");
383 }
384 
386 {
387  PythonConfig config;
388 
389  QString root = qEnvironmentVariable("CONDA_PREFIX");
390  if (!root.isEmpty())
391  {
392  const QString pythonExePath = PathToPythonExecutableInEnv(Type::Conda, root);
393  config.initFromPythonExecutable(pythonExePath);
394  config.m_type = Type::Conda;
395  return config;
396  }
397 
398  root = qEnvironmentVariable("VIRTUAL_ENV");
399  if (!root.isEmpty())
400  {
401  const QString pythonExePath = PathToPythonExecutableInEnv(Type::Venv, root);
402  config.initFromPythonExecutable(pythonExePath);
403  config.m_type = Type::Venv;
404  return config;
405  }
406 
407  return config;
408 }
409 
410 void PythonConfig::initFromPythonExecutable(const QString &pythonExecutable)
411 {
412  m_type = Type::Bundled;
413 
414  const QString pythonPathScript = QStringLiteral(
415  "import os;import sys;print(os.pathsep.join(sys.path[1:]));print(sys.prefix, end='')");
416 
417  QProcess pythonProcess;
418  pythonProcess.setProgram(pythonExecutable);
419  pythonProcess.setArguments({"-c", pythonPathScript});
420  pythonProcess.start(QIODevice::ReadOnly);
421  pythonProcess.waitForFinished();
422 
423  const QString result =
424 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
425  QString::fromUtf8(pythonProcess.readAllStandardOutput());
426 #else
427  qtCompatCodecForName("utf-8")->toUnicode(pythonProcess.readAllStandardOutput());
428 #endif
429 
430  QStringList pathsAndHome = result.split('\n');
431 
432  if (pathsAndHome.size() != 2)
433  {
434  plgPrint() << "pythonExecutable: " << pythonExecutable;
435  plgWarning() << "'" << result << "' could not be parsed as a list if paths and a home path."
436  << "Expected 2 strings found " << pathsAndHome.size();
437  return;
438  }
439 
440  m_pythonPath = pathsAndHome.takeFirst();
441  m_pythonHome = pathsAndHome.takeFirst();
442 
443 #if defined(USE_EMBEDDED_MODULES)
444  m_pythonPath.append(BundledSitePackagesPath());
445 #endif
446 }
std::string name
static QString PathToPythonExecutableInEnv(PythonConfig::Type envType, const QString &envRoot)
static Version GetPythonExeVersion(QProcess &pythonProcess)
constexpr Version PythonVersion(PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION)
Python Version the plugin was compiled against.
QtCompatTextCodec * qtCompatCodecForName(const char *name)
Definition: QtCompat.h:657
QString qtCompatStringRefToString(const QStringView &view)
Definition: QtCompat.h:236
QStringView QtCompatStringRef
Definition: QtCompat.h:227
QtCompatStringRefList qtCompatSplitRefChar(const QString &str, QChar sep)
Definition: QtCompat.h:430
QStringView qtCompatStringRef(const QString &str) noexcept
Definition: QtCompat.h:231
PluginLogger< CVLog::LOG_WARNING > plgWarning
Definition: Utilities.h:102
wchar_t * QStringToWcharArray(const QString &string)
Returns a newly allocated wchar_t array (null terminated) from a QString.
Definition: Utilities.h:106
PluginLogger< CVLog::LOG_STANDARD > plgPrint
Definition: Utilities.h:100
core::Tensor result
Definition: VtkUtils.cpp:76
const wchar_t * pythonPath() const
Returns the pythonPath.
const wchar_t * pythonHome() const
Returns the pythonHome.
bool isSet() const
returns true if both paths are non empty
void initBundled()
Version getVersion() const
static PythonConfig fromContainingEnvironment()
bool validateAndDisplayErrors(QWidget *parent=nullptr) const
Type type() const
Definition: PythonConfig.h:84
PythonConfigPaths pythonCompatiblePaths() const
void initFromLocation(const QString &prefix)
void preparePythonProcess(QProcess &pythonProcess) const
void initFromPythonExecutable(const QString &pythonExecutable)
static bool IsInsideEnvironment()
void initDefault()
void initCondaEnv(const QString &condaPrefix)
Initialize the paths to use the conda environment stored at condaPrefix.
void initVenv(const QString &venvPrefix)
Initialize the paths to use the python venv stored at venvPrefix.
QString toUnicode(const char *chars, int len=-1)
Definition: QtCompat.h:610
static const std::string path
Definition: PointCloud.cpp:59
std::string toString(T x)
Definition: Common.h:80
Version version
PyVenvCfg()=default
QString home
static PyVenvCfg FromFile(const QString &path)
bool includeSystemSitesPackages
Simple representation of a SemVer version.
Definition: PythonConfig.h:28
bool isNull() const
Definition: PythonConfig.h:51
constexpr Version()=default
bool operator==(const Version &other) const
uint16_t versionPatch
Definition: PythonConfig.h:60
bool isCompatibleWithCompiledVersion() const
uint16_t versionMinor
Definition: PythonConfig.h:59
uint16_t versionMajor
Definition: PythonConfig.h:58