// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "pythonbuildconfiguration.h" #include "pipsupport.h" #include "pyside.h" #include "pysideuicextracompiler.h" #include "pythonconstants.h" #include "pythoneditor.h" #include "pythonkitaspect.h" #include "pythonlanguageclient.h" #include "pythonproject.h" #include "pythonsettings.h" #include "pythontr.h" #include "pythonutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace Utils; namespace Python::Internal { PySideBuildStep::PySideBuildStep(BuildStepList *bsl, Id id) : AbstractProcessStep(bsl, id) { m_pysideProject.setSettingsKey("Python.PySideProjectTool"); m_pysideProject.setLabelText(Tr::tr("PySide project tool:")); m_pysideProject.setToolTip(Tr::tr("Enter location of PySide project tool.")); m_pysideProject.setExpectedKind(PathChooser::Command); m_pysideProject.setHistoryCompleter("Python.PySideProjectTool.History"); m_pysideProject.setReadOnly(true); m_pysideUic.setSettingsKey("Python.PySideUic"); m_pysideUic.setLabelText(Tr::tr("PySide uic tool:")); m_pysideUic.setToolTip(Tr::tr("Enter location of PySide uic tool.")); m_pysideUic.setExpectedKind(PathChooser::Command); m_pysideUic.setHistoryCompleter("Python.PySideUic.History"); m_pysideUic.setReadOnly(true); setCommandLineProvider([this] { return CommandLine(m_pysideProject(), {"build"}); }); setWorkingDirectoryProvider([this] { return m_pysideProject().withNewMappedPath(project()->projectDirectory()); // FIXME: new path needed? }); setEnvironmentModifier([this](Environment &env) { env.prependOrSetPath(m_pysideProject().parentDir()); }); connect(buildSystem(), &BuildSystem::updated, this, &PySideBuildStep::updateExtraCompilers); connect(&m_pysideUic, &BaseAspect::changed, this, &PySideBuildStep::updateExtraCompilers); } PySideBuildStep::~PySideBuildStep() { qDeleteAll(m_extraCompilers); } void PySideBuildStep::checkForPySide(const FilePath &python) { PySideTools tools; if (python.isEmpty() || !python.isExecutableFile()) { m_pysideProject.setValue(FilePath()); m_pysideUic.setValue(FilePath()); return; } const FilePath dir = python.parentDir(); tools.pySideProjectPath = dir.pathAppended("pyside6-project").withExecutableSuffix(); tools.pySideUicPath = dir.pathAppended("pyside6-uic").withExecutableSuffix(); if (tools.pySideProjectPath.isExecutableFile() && tools.pySideUicPath.isExecutableFile()) { m_pysideProject.setValue(tools.pySideProjectPath.toUserOutput()); m_pysideUic.setValue(tools.pySideUicPath.toUserOutput()); } else { checkForPySide(python, "PySide6-Essentials"); } } void PySideBuildStep::checkForPySide(const FilePath &python, const QString &pySidePackageName) { const PipPackage package(pySidePackageName); QObject::disconnect(m_watcherConnection); m_watcher.reset(new QFutureWatcher()); m_watcherConnection = QObject::connect(m_watcher.get(), &QFutureWatcherBase::finished, this, [this, python, pySidePackageName] { handlePySidePackageInfo(m_watcher->result(), python, pySidePackageName); }); const auto future = Pip::instance(python)->info(package); m_watcher->setFuture(future); Utils::futureSynchronizer()->addFuture(future); } void PySideBuildStep::handlePySidePackageInfo(const PipPackageInfo &pySideInfo, const FilePath &python, const QString &requestedPackageName) { const auto findPythonTools = [](const FilePaths &files, const FilePath &location, const FilePath &python) -> PySideTools { PySideTools result; const QString pySide6ProjectName = OsSpecificAspects::withExecutableSuffix(python.osType(), "pyside6-project"); const QString pySide6UicName = OsSpecificAspects::withExecutableSuffix(python.osType(), "pyside6-uic"); for (const FilePath &file : files) { if (file.fileName() == pySide6ProjectName) { result.pySideProjectPath = python.withNewMappedPath(location.resolvePath(file)); result.pySideProjectPath = result.pySideProjectPath.cleanPath(); if (!result.pySideUicPath.isEmpty()) return result; } else if (file.fileName() == pySide6UicName) { result.pySideUicPath = python.withNewMappedPath(location.resolvePath(file)); result.pySideUicPath = result.pySideUicPath.cleanPath(); if (!result.pySideProjectPath.isEmpty()) return result; } } return {}; }; PySideTools tools = findPythonTools(pySideInfo.files, pySideInfo.location, python); if (!tools.pySideProjectPath.isExecutableFile() && requestedPackageName != "PySide6") { checkForPySide(python, "PySide6"); return; } m_pysideProject.setValue(tools.pySideProjectPath.toUserOutput()); m_pysideUic.setValue(tools.pySideUicPath.toUserOutput()); } Tasking::GroupItem PySideBuildStep::runRecipe() { using namespace Tasking; const auto onSetup = [this] { if (!processParameters()->effectiveCommand().isExecutableFile()) return SetupResult::StopWithSuccess; return SetupResult::Continue; }; return Group { onGroupSetup(onSetup), defaultProcessTask() }; } void PySideBuildStep::updateExtraCompilers() { QList oldCompilers = m_extraCompilers; m_extraCompilers.clear(); if (m_pysideUic().isExecutableFile()) { auto uiMatcher = [](const Node *node) { if (const FileNode *fileNode = node->asFileNode()) return fileNode->fileType() == FileType::Form; return false; }; const FilePaths uiFiles = project()->files(uiMatcher); for (const FilePath &uiFile : uiFiles) { FilePath generated = uiFile.parentDir(); generated = generated.pathAppended("/ui_" + uiFile.baseName() + ".py"); int index = Utils::indexOf(oldCompilers, [&](PySideUicExtraCompiler *oldCompiler) { return oldCompiler->pySideUicPath() == m_pysideUic() && oldCompiler->project() == project() && oldCompiler->source() == uiFile && oldCompiler->targets() == FilePaths{generated}; }); if (index < 0) { m_extraCompilers << new PySideUicExtraCompiler(m_pysideUic(), project(), uiFile, {generated}, this); } else { m_extraCompilers << oldCompilers.takeAt(index); } } } for (LanguageClient::Client *client : LanguageClient::LanguageClientManager::clients()) { if (auto pylsClient = qobject_cast(client)) pylsClient->updateExtraCompilers(m_extraCompilers); } qDeleteAll(oldCompilers); } QList PySideBuildStep::extraCompilers() const { return m_extraCompilers; } Id PySideBuildStep::id() { return Id("Python.PysideBuildStep"); } class PythonBuildSettingsWidget : public QWidget { public: PythonBuildSettingsWidget(PythonBuildConfiguration *bc) { using namespace Layouting; m_configureDetailsWidget = new DetailsWidget; m_configureDetailsWidget->setSummaryText(bc->python().toUserOutput()); if (const std::optional venv = bc->venv()) { auto details = new QWidget(); Form{Tr::tr("Effective venv:"), venv->toUserOutput(), br}.attachTo(details); m_configureDetailsWidget->setWidget(details); } else { m_configureDetailsWidget->setState(DetailsWidget::OnlySummary); } Column{ m_configureDetailsWidget, noMargin }.attachTo(this); } private: DetailsWidget *m_configureDetailsWidget; }; class PySideBuildStepFactory final : public BuildStepFactory { public: PySideBuildStepFactory() { registerStep(PySideBuildStep::id()); setSupportedProjectType(PythonProjectId); setDisplayName(Tr::tr("Run PySide6 project tool")); setFlags(BuildStep::UniqueStep); } }; void setupPySideBuildStep() { static PySideBuildStepFactory thePySideBuildStepFactory; } // PythonBuildConfiguration PythonBuildConfiguration::PythonBuildConfiguration(Target *target, const Id &id) : BuildConfiguration(target, id) { setInitializer([this](const BuildInfo &info) { initialize(info); }); setConfigWidgetDisplayName(Tr::tr("Python")); updateCacheAndEmitEnvironmentChanged(); connect(&PySideInstaller::instance(), &PySideInstaller::pySideInstalled, this, &PythonBuildConfiguration::handlePythonUpdated); auto update = [this] { if (isActive()) { buildSystem()->emitBuildSystemUpdated(); updateDocuments(); } }; connect(target, &Target::activeBuildConfigurationChanged, this, update); connect(project(), &Project::activeTargetChanged, this, update); connect(ProjectExplorerPlugin::instance(), &ProjectExplorerPlugin::fileListChanged, this, update); connect(PythonSettings::instance(), &PythonSettings::virtualEnvironmentCreated, this, &PythonBuildConfiguration::handlePythonUpdated); } QWidget *PythonBuildConfiguration::createConfigWidget() { return new PythonBuildSettingsWidget(this); } static QString venvTypeName() { static QString name = Tr::tr("New Virtual Environment"); return name; } void PythonBuildConfiguration::initialize(const BuildInfo &info) { buildSteps()->appendStep(PySideBuildStep::id()); if (info.typeName == venvTypeName()) { m_venv = info.buildDirectory; const FilePath venvInterpreterPath = info.buildDirectory.resolvePath( HostOsInfo::isWindowsHost() ? FilePath::fromUserInput("Scripts/python.exe") : FilePath::fromUserInput("bin/python")); updatePython(venvInterpreterPath); if (info.extraInfo.toMap().value("createVenv", false).toBool() && !info.buildDirectory.exists()) { if (std::optional python = PythonKitAspect::python(kit())) PythonSettings::createVirtualEnvironment(python->command, info.buildDirectory); } } else { updateInterpreter(PythonKitAspect::python(kit())); } updateCacheAndEmitEnvironmentChanged(); } void PythonBuildConfiguration::updateInterpreter(const std::optional &python) { updatePython(python ? python->command : FilePath()); } void PythonBuildConfiguration::updatePython(const FilePath &python) { m_python = python; if (auto buildStep = buildSteps()->firstOfType()) buildStep->checkForPySide(python); updateDocuments(); buildSystem()->requestParse(); } void PythonBuildConfiguration::updateDocuments() { if (isActive()) { const FilePaths files = project()->files(Project::AllFiles); for (const FilePath &file : files) { if (auto doc = TextEditor::TextDocument::textDocumentForFilePath(file)) { if (auto pyDoc = qobject_cast(doc)) pyDoc->updatePython(m_python); else if (doc->mimeType() == Utils::Constants::QML_MIMETYPE) PySideInstaller::instance().checkPySideInstallation(m_python, doc); } } } } void PythonBuildConfiguration::handlePythonUpdated(const FilePath &python) { if (!m_python.isEmpty() && python == m_python) updatePython(python); // retrigger pyside check } static const char pythonKey[] = "python"; static const char venvKey[] = "venv"; void PythonBuildConfiguration::fromMap(const Store &map) { BuildConfiguration::fromMap(map); if (map.contains(venvKey)) m_venv = FilePath::fromSettings(map[venvKey]); updatePython(FilePath::fromSettings(map[pythonKey])); } void PythonBuildConfiguration::toMap(Store &map) const { BuildConfiguration::toMap(map); map[pythonKey] = m_python.toSettings(); if (m_venv) map[venvKey] = m_venv->toSettings(); } FilePath PythonBuildConfiguration::python() const { return m_python; } std::optional PythonBuildConfiguration::venv() const { return m_venv; } class PythonBuildConfigurationFactory final : public BuildConfigurationFactory { public: PythonBuildConfigurationFactory() { registerBuildConfiguration("Python.PySideBuildConfiguration"); setSupportedProjectType(PythonProjectId); setSupportedProjectMimeTypeNames( {Constants::C_PY_PROJECT_MIME_TYPE, Constants::C_PY_PROJECT_MIME_TYPE_TOML}); setBuildGenerator([](const Kit *k, const FilePath &projectPath, bool forSetup) { if (std::optional python = PythonKitAspect::python(k)) { BuildInfo base; base.buildDirectory = projectPath.parentDir(); base.displayName = python->name; base.typeName = Tr::tr("Global Python"); base.showBuildDirConfigWidget = false; if (isVenvPython(python->command) || !venvIsUsable(python->command)) return QList{base}; base.enabledByDefault = false; BuildInfo venv; const FilePath venvBase = projectPath.parentDir() / ".qtcreator" / FileUtils::fileSystemFriendlyName(python->name + "venv"); venv.buildDirectory = venvBase; int i = 2; while (venv.buildDirectory.exists()) venv.buildDirectory = venvBase.stringAppended('_' + QString::number(i++)); //: %1 = name of this Python as registered in QtC venv.displayName = Tr::tr("%1 Virtual Environment").arg(python->name); venv.typeName = venvTypeName(); venv.extraInfo = QVariantMap{{"createVenv", forSetup}}; return QList{base, venv}; } return QList{}; }); } }; void setupPythonBuildConfiguration() { static PythonBuildConfigurationFactory thePythonBuildConfigurationFactory; } } // Python::Internal