Add checks for minimum supported Python version
Now it checks for all python installations and related procedures has been revised. If the python version does not meet the minimum requirement, it will be logged. PR #22729.
This commit is contained in:
parent
83799f4f07
commit
d56b353c52
@ -30,6 +30,7 @@
|
|||||||
#include "foreignapps.h"
|
#include "foreignapps.h"
|
||||||
|
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
|
#include <algorithm>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -40,14 +41,18 @@
|
|||||||
|
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QScopeGuard>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "base/global.h"
|
|
||||||
#include "base/logger.h"
|
#include "base/logger.h"
|
||||||
#include "base/path.h"
|
#include "base/path.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
#include "base/utils/bytearray.h"
|
#include "base/utils/bytearray.h"
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
#include "base/utils/compare.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace Utils::ForeignApps;
|
using namespace Utils::ForeignApps;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@ -106,23 +111,23 @@ namespace
|
|||||||
|
|
||||||
DWORD cSubKeys = 0;
|
DWORD cSubKeys = 0;
|
||||||
DWORD cMaxSubKeyLen = 0;
|
DWORD cMaxSubKeyLen = 0;
|
||||||
LONG res = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL);
|
const LSTATUS result = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
if (res == ERROR_SUCCESS)
|
if (result == ERROR_SUCCESS)
|
||||||
{
|
{
|
||||||
++cMaxSubKeyLen; // For null character
|
++cMaxSubKeyLen; // For null character
|
||||||
LPWSTR lpName = new WCHAR[cMaxSubKeyLen];
|
LPWSTR lpName = new WCHAR[cMaxSubKeyLen];
|
||||||
DWORD cName;
|
[[maybe_unused]] const auto lpNameGuard = qScopeGuard([&lpName] { delete[] lpName; });
|
||||||
|
|
||||||
|
keys.reserve(cSubKeys);
|
||||||
|
|
||||||
for (DWORD i = 0; i < cSubKeys; ++i)
|
for (DWORD i = 0; i < cSubKeys; ++i)
|
||||||
{
|
{
|
||||||
cName = cMaxSubKeyLen;
|
DWORD cName = cMaxSubKeyLen;
|
||||||
res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL);
|
const LSTATUS res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL);
|
||||||
if (res == ERROR_SUCCESS)
|
if (res == ERROR_SUCCESS)
|
||||||
keys.push_back(QString::fromWCharArray(lpName));
|
keys.append(QString::fromWCharArray(lpName));
|
||||||
}
|
}
|
||||||
|
|
||||||
delete[] lpName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys;
|
return keys;
|
||||||
@ -133,117 +138,84 @@ namespace
|
|||||||
const std::wstring nameWStr = name.toStdWString();
|
const std::wstring nameWStr = name.toStdWString();
|
||||||
DWORD type = 0;
|
DWORD type = 0;
|
||||||
DWORD cbData = 0;
|
DWORD cbData = 0;
|
||||||
|
|
||||||
// Discover the size of the value
|
// Discover the size of the value
|
||||||
::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, NULL, &cbData);
|
::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, NULL, &cbData);
|
||||||
DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
|
|
||||||
LPWSTR lpData = new WCHAR[cBuffer];
|
|
||||||
LONG res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
|
|
||||||
|
|
||||||
QString result;
|
const DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
|
||||||
|
LPWSTR lpData = new WCHAR[cBuffer]{0};
|
||||||
|
[[maybe_unused]] const auto lpDataGuard = qScopeGuard([&lpData] { delete[] lpData; });
|
||||||
|
|
||||||
|
const LSTATUS res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
|
||||||
if (res == ERROR_SUCCESS)
|
if (res == ERROR_SUCCESS)
|
||||||
{
|
return QString::fromWCharArray(lpData);
|
||||||
lpData[cBuffer - 1] = 0;
|
|
||||||
result = QString::fromWCharArray(lpData);
|
|
||||||
}
|
|
||||||
delete[] lpData;
|
|
||||||
|
|
||||||
return result;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString pythonSearchReg(const REG_SEARCH_TYPE type)
|
QStringList pythonSearchReg(const REG_SEARCH_TYPE type)
|
||||||
{
|
{
|
||||||
HKEY hkRoot;
|
const HKEY hkRoot = (type == USER) ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||||
if (type == USER)
|
const REGSAM samDesired = KEY_READ
|
||||||
hkRoot = HKEY_CURRENT_USER;
|
| ((type == SYSTEM_64BIT) ? KEY_WOW64_64KEY : KEY_WOW64_32KEY);
|
||||||
else
|
QStringList ret;
|
||||||
hkRoot = HKEY_LOCAL_MACHINE;
|
|
||||||
|
|
||||||
REGSAM samDesired = KEY_READ;
|
HKEY hkPythonCore {0};
|
||||||
if (type == SYSTEM_32BIT)
|
if (::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore) == ERROR_SUCCESS)
|
||||||
samDesired |= KEY_WOW64_32KEY;
|
|
||||||
else if (type == SYSTEM_64BIT)
|
|
||||||
samDesired |= KEY_WOW64_64KEY;
|
|
||||||
|
|
||||||
QString path;
|
|
||||||
LONG res = 0;
|
|
||||||
HKEY hkPythonCore;
|
|
||||||
res = ::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore);
|
|
||||||
|
|
||||||
if (res == ERROR_SUCCESS)
|
|
||||||
{
|
{
|
||||||
|
[[maybe_unused]] const auto hkPythonCoreGuard = qScopeGuard([&hkPythonCore] { ::RegCloseKey(hkPythonCore); });
|
||||||
|
|
||||||
|
// start with the largest version
|
||||||
QStringList versions = getRegSubkeys(hkPythonCore);
|
QStringList versions = getRegSubkeys(hkPythonCore);
|
||||||
versions.sort();
|
// ordinary sort won't suffice, it needs to sort ["3.9", "3.10"] correctly
|
||||||
|
std::sort(versions.begin(), versions.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
|
||||||
|
ret.reserve(versions.size() * 2);
|
||||||
|
|
||||||
bool found = false;
|
while (!versions.empty())
|
||||||
while (!found && !versions.empty())
|
|
||||||
{
|
{
|
||||||
const std::wstring version = QString(versions.takeLast() + u"\\InstallPath").toStdWString();
|
const std::wstring version = QString(versions.takeLast() + u"\\InstallPath").toStdWString();
|
||||||
|
|
||||||
HKEY hkInstallPath;
|
HKEY hkInstallPath {0};
|
||||||
res = ::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath);
|
if (::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath) == ERROR_SUCCESS)
|
||||||
|
|
||||||
if (res == ERROR_SUCCESS)
|
|
||||||
{
|
{
|
||||||
qDebug("Detected possible Python v%ls location", version.c_str());
|
[[maybe_unused]] const auto hkInstallPathGuard = qScopeGuard([&hkInstallPath] { ::RegCloseKey(hkInstallPath); });
|
||||||
path = getRegValue(hkInstallPath);
|
|
||||||
::RegCloseKey(hkInstallPath);
|
|
||||||
|
|
||||||
if (!path.isEmpty())
|
const QString basePath = getRegValue(hkInstallPath);
|
||||||
|
if (basePath.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (const QString path = (basePath + u"python3.exe"); QFile::exists(path))
|
||||||
|
ret.append(path);
|
||||||
|
if (const QString path = (basePath + u"python.exe"); QFile::exists(path))
|
||||||
|
ret.append(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList searchPythonPaths()
|
||||||
{
|
{
|
||||||
const QDir baseDir {path};
|
QStringList ret;
|
||||||
|
|
||||||
if (baseDir.exists(u"python3.exe"_s))
|
// From registry
|
||||||
{
|
ret.append(pythonSearchReg(USER));
|
||||||
found = true;
|
ret.append(pythonSearchReg(SYSTEM_64BIT));
|
||||||
path = baseDir.filePath(u"python3.exe"_s);
|
ret.append(pythonSearchReg(SYSTEM_32BIT));
|
||||||
}
|
|
||||||
else if (baseDir.exists(u"python.exe"_s))
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
path = baseDir.filePath(u"python.exe"_s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
path = QString();
|
|
||||||
|
|
||||||
::RegCloseKey(hkPythonCore);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString findPythonPath()
|
|
||||||
{
|
|
||||||
QString path = pythonSearchReg(USER);
|
|
||||||
if (!path.isEmpty())
|
|
||||||
return path;
|
|
||||||
|
|
||||||
path = pythonSearchReg(SYSTEM_32BIT);
|
|
||||||
if (!path.isEmpty())
|
|
||||||
return path;
|
|
||||||
|
|
||||||
path = pythonSearchReg(SYSTEM_64BIT);
|
|
||||||
if (!path.isEmpty())
|
|
||||||
return path;
|
|
||||||
|
|
||||||
// Fallback: Detect python from default locations
|
// Fallback: Detect python from default locations
|
||||||
const QFileInfoList dirs = QDir(u"C:/"_s).entryInfoList({u"Python*"_s}, QDir::Dirs, (QDir::Name | QDir::Reversed));
|
const QFileInfoList dirs = QDir(u"C:/"_s).entryInfoList({u"Python*"_s}, QDir::Dirs, (QDir::Name | QDir::Reversed));
|
||||||
for (const QFileInfo &info : dirs)
|
for (const QFileInfo &info : dirs)
|
||||||
{
|
{
|
||||||
const QString py3Path {info.absolutePath() + u"/python3.exe"};
|
const QString absPath = info.absolutePath();
|
||||||
if (QFile::exists(py3Path))
|
|
||||||
return py3Path;
|
|
||||||
|
|
||||||
const QString pyPath {info.absolutePath() + u"/python.exe"};
|
if (const QString path = (absPath + u"/python3.exe"); QFile::exists(path))
|
||||||
if (QFile::exists(pyPath))
|
ret.append(path);
|
||||||
return pyPath;
|
if (const QString path = (absPath + u"/python.exe"); QFile::exists(path))
|
||||||
|
ret.append(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return ret;
|
||||||
}
|
}
|
||||||
#endif // Q_OS_WIN
|
#endif // Q_OS_WIN
|
||||||
}
|
}
|
||||||
@ -255,7 +227,7 @@ bool Utils::ForeignApps::PythonInfo::isValid() const
|
|||||||
|
|
||||||
bool Utils::ForeignApps::PythonInfo::isSupportedVersion() const
|
bool Utils::ForeignApps::PythonInfo::isSupportedVersion() const
|
||||||
{
|
{
|
||||||
return (version >= Version {3, 9, 0});
|
return (version >= MINIMUM_SUPPORTED_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
PythonInfo Utils::ForeignApps::pythonInfo()
|
PythonInfo Utils::ForeignApps::pythonInfo()
|
||||||
@ -266,37 +238,61 @@ PythonInfo Utils::ForeignApps::pythonInfo()
|
|||||||
if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executableName))
|
if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executableName))
|
||||||
return pyInfo;
|
return pyInfo;
|
||||||
|
|
||||||
|
const QString invalidVersionMessage = QCoreApplication::translate("Utils::ForeignApps"
|
||||||
|
, "Python failed to meet minimum version requirement. Path: \"%1\". Found version: \"%2\". Minimum supported version: \"%3\".");
|
||||||
|
|
||||||
if (!preferredPythonPath.isEmpty())
|
if (!preferredPythonPath.isEmpty())
|
||||||
{
|
{
|
||||||
if (testPythonInstallation(preferredPythonPath, pyInfo))
|
if (testPythonInstallation(preferredPythonPath, pyInfo))
|
||||||
|
{
|
||||||
|
if (pyInfo.isSupportedVersion())
|
||||||
return pyInfo;
|
return pyInfo;
|
||||||
|
|
||||||
|
LogMsg(invalidVersionMessage.arg(pyInfo.executableName, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::WARNING);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".")
|
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".")
|
||||||
.arg(preferredPythonPath), Log::WARNING);
|
.arg(preferredPythonPath), Log::WARNING);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// auto detect only when there are no preferred python path
|
// auto detect only when there are no preferred python path
|
||||||
|
|
||||||
if (!pyInfo.isValid())
|
if (!pyInfo.isValid())
|
||||||
{
|
{
|
||||||
if (testPythonInstallation(u"python3"_s, pyInfo))
|
const QString exeNames[] = {u"python3"_s, u"python"_s};
|
||||||
|
for (const QString &exeName : exeNames)
|
||||||
|
{
|
||||||
|
if (testPythonInstallation(exeName, pyInfo))
|
||||||
|
{
|
||||||
|
if (pyInfo.isSupportedVersion())
|
||||||
return pyInfo;
|
return pyInfo;
|
||||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python3` executable in PATH environment variable. PATH: \"%1\"")
|
|
||||||
.arg(qEnvironmentVariable("PATH")), Log::INFO);
|
|
||||||
|
|
||||||
if (testPythonInstallation(u"python"_s, pyInfo))
|
LogMsg(invalidVersionMessage.arg(pyInfo.executableName, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
|
||||||
return pyInfo;
|
}
|
||||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in PATH environment variable. PATH: \"%1\"")
|
else
|
||||||
.arg(qEnvironmentVariable("PATH")), Log::INFO);
|
{
|
||||||
|
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `%1` executable in PATH environment variable. PATH: \"%2\"")
|
||||||
|
.arg(exeName, qEnvironmentVariable("PATH")), Log::INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
if (testPythonInstallation(findPythonPath(), pyInfo))
|
for (const QString &path : asConst(searchPythonPaths()))
|
||||||
|
{
|
||||||
|
if (testPythonInstallation(path, pyInfo))
|
||||||
|
{
|
||||||
|
if (pyInfo.isSupportedVersion())
|
||||||
return pyInfo;
|
return pyInfo;
|
||||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in Windows Registry."), Log::INFO);
|
|
||||||
|
LogMsg(invalidVersionMessage.arg(pyInfo.executableName, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable"), Log::WARNING);
|
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable"), Log::WARNING);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ namespace Utils::ForeignApps
|
|||||||
|
|
||||||
QString executableName;
|
QString executableName;
|
||||||
Version version;
|
Version version;
|
||||||
|
|
||||||
|
inline static const Version MINIMUM_SUPPORTED_VERSION {3, 9, 0};
|
||||||
};
|
};
|
||||||
|
|
||||||
PythonInfo pythonInfo();
|
PythonInfo pythonInfo();
|
||||||
|
@ -1616,14 +1616,14 @@ void MainWindow::on_actionSearchWidget_triggered()
|
|||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
|
const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
|
||||||
, tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
|
, tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
|
||||||
.arg(pyInfo.version.toString(), u"3.9.0")
|
.arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString())
|
||||||
, (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
|
, (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
|
||||||
if (buttonPressed == QMessageBox::Yes)
|
if (buttonPressed == QMessageBox::Yes)
|
||||||
installPython();
|
installPython();
|
||||||
#else
|
#else
|
||||||
QMessageBox::information(this, tr("Old Python Runtime")
|
QMessageBox::information(this, tr("Old Python Runtime")
|
||||||
, tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
|
, tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
|
||||||
.arg(pyInfo.version.toString(), u"3.9.0"));
|
.arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()));
|
||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user