Files
qt-creator/src/plugins/python/pythonproject.cpp

497 lines
16 KiB
C++
Raw Normal View History

// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "pythonproject.h"
#include "pythonconstants.h"
#include "pythontr.h"
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/buildtargetinfo.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/target.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTimer>
#include <coreplugin/documentmanager.h>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <qmljs/qmljsmodelmanagerinterface.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
#include <utils/mimeutils.h>
using namespace Core;
using namespace ProjectExplorer;
using namespace Utils;
namespace Python::Internal {
class PythonBuildSystem : public BuildSystem
{
public:
explicit PythonBuildSystem(Target *target);
bool supportsAction(Node *context, ProjectAction action, const Node *node) const override;
bool addFiles(Node *, const FilePaths &filePaths, FilePaths *) override;
RemovedFilesFromProject removeFiles(Node *, const FilePaths &filePaths, FilePaths *) override;
bool deleteFiles(Node *, const FilePaths &) override;
bool renameFile(Node *,
const FilePath &oldFilePath,
const FilePath &newFilePath) override;
QString name() const override { return QLatin1String("python"); }
bool saveRawFileList(const QStringList &rawFileList);
bool saveRawList(const QStringList &rawList, const QString &fileName);
void parse();
QStringList processEntries(const QStringList &paths,
QHash<QString, QString> *map = nullptr) const;
bool writePyProjectFile(const QString &fileName, QString &content,
const QStringList &rawList, QString *errorMessage);
void triggerParsing() final;
private:
QStringList m_rawFileList;
QStringList m_files;
QStringList m_rawQmlImportPathList;
QStringList m_qmlImportPaths;
QHash<QString, QString> m_rawListEntries;
QHash<QString, QString> m_rawQmlImportPathEntries;
};
/**
* @brief Provides displayName relative to project node
*/
class PythonFileNode : public FileNode
{
public:
PythonFileNode(const FilePath &filePath, const QString &nodeDisplayName,
FileType fileType = FileType::Source)
: FileNode(filePath, fileType)
, m_displayName(nodeDisplayName)
{}
QString displayName() const override { return m_displayName; }
private:
QString m_displayName;
};
static QJsonObject readObjJson(const FilePath &projectFile, QString *errorMessage)
{
QFile file(projectFile.toString());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
*errorMessage = Tr::tr("Unable to open \"%1\" for reading: %2")
.arg(projectFile.toUserOutput(), file.errorString());
return QJsonObject();
}
const QByteArray content = file.readAll();
// This assumes the project file is formed with only one field called
// 'files' that has a list associated of the files to include in the project.
if (content.isEmpty()) {
*errorMessage = Tr::tr("Unable to read \"%1\": The file is empty.")
.arg(projectFile.toUserOutput());
return QJsonObject();
}
QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(content, &error);
if (doc.isNull()) {
const int line = content.left(error.offset).count('\n') + 1;
*errorMessage = Tr::tr("Unable to parse \"%1\":%2: %3")
.arg(projectFile.toUserOutput()).arg(line)
.arg(error.errorString());
return QJsonObject();
}
return doc.object();
}
static QStringList readLines(const FilePath &projectFile)
{
const QString projectFileName = projectFile.fileName();
QSet<QString> visited = { projectFileName };
QStringList lines = { projectFileName };
QFile file(projectFile.toString());
if (file.open(QFile::ReadOnly)) {
QTextStream stream(&file);
while (true) {
const QString line = stream.readLine();
if (line.isNull())
break;
if (visited.contains(line))
continue;
lines.append(line);
visited.insert(line);
}
}
return lines;
}
static QStringList readLinesJson(const FilePath &projectFile, QString *errorMessage)
{
QStringList lines = { projectFile.fileName() };
const QJsonObject obj = readObjJson(projectFile, errorMessage);
if (obj.contains("files")) {
const QJsonValue files = obj.value("files");
const QJsonArray files_array = files.toArray();
QSet<QString> visited;
for (const auto &file : files_array)
visited.insert(file.toString());
lines.append(Utils::toList(visited));
}
return lines;
}
static QStringList readImportPathsJson(const FilePath &projectFile, QString *errorMessage)
{
QStringList importPaths;
const QJsonObject obj = readObjJson(projectFile, errorMessage);
if (obj.contains("qmlImportPaths")) {
const QJsonValue dirs = obj.value("qmlImportPaths");
const QJsonArray dirs_array = dirs.toArray();
QSet<QString> visited;
for (const auto &dir : dirs_array)
visited.insert(dir.toString());
importPaths.append(Utils::toList(visited));
}
return importPaths;
}
class PythonProjectNode : public ProjectNode
{
public:
PythonProjectNode(const FilePath &path)
: ProjectNode(path)
{
setDisplayName(path.completeBaseName());
setAddFileFilter("*.py");
}
};
PythonProject::PythonProject(const FilePath &fileName)
: Project(Constants::C_PY_MIMETYPE, fileName)
{
setId(PythonProjectId);
setProjectLanguages(Context(ProjectExplorer::Constants::PYTHON_LANGUAGE_ID));
setDisplayName(fileName.completeBaseName());
setBuildSystemCreator([](Target *t) { return new PythonBuildSystem(t); });
}
static FileType getFileType(const FilePath &f)
{
if (f.endsWith(".py"))
return FileType::Source;
if (f.endsWith(".pyproject") || f.endsWith(".pyqtc"))
return FileType::Project;
if (f.endsWith(".qrc"))
return FileType::Resource;
if (f.endsWith(".ui"))
return FileType::Form;
if (f.endsWith(".qml") || f.endsWith(".js"))
return FileType::QML;
return Node::fileTypeForFileName(f);
}
void PythonBuildSystem::triggerParsing()
{
ParseGuard guard = guardParsingRun();
parse();
const QDir baseDir(projectDirectory().toString());
QList<BuildTargetInfo> appTargets;
auto newRoot = std::make_unique<PythonProjectNode>(projectDirectory());
for (const QString &f : std::as_const(m_files)) {
const QString displayName = baseDir.relativeFilePath(f);
const FilePath filePath = FilePath::fromString(f);
const FileType fileType = getFileType(filePath);
newRoot->addNestedNode(std::make_unique<PythonFileNode>(filePath, displayName, fileType));
const MimeType mt = mimeTypeForFile(filePath, MimeMatchMode::MatchExtension);
if (mt.matchesName(Constants::C_PY_MIMETYPE) || mt.matchesName(Constants::C_PY3_MIMETYPE)) {
BuildTargetInfo bti;
bti.displayName = displayName;
bti.buildKey = f;
bti.targetFilePath = filePath;
bti.projectFilePath = projectFilePath();
bti.isQtcRunnable = filePath.fileName() == "main.py";
appTargets.append(bti);
}
}
setRootProjectNode(std::move(newRoot));
setApplicationTargets(appTargets);
auto modelManager = QmlJS::ModelManagerInterface::instance();
if (modelManager) {
auto projectInfo = modelManager->defaultProjectInfoForProject(project());
for (const QString &importPath : std::as_const(m_qmlImportPaths)) {
const FilePath filePath = FilePath::fromString(importPath);
projectInfo.importPaths.maybeInsert(filePath, QmlJS::Dialect::Qml);
}
modelManager->updateProjectInfo(projectInfo, project());
}
guard.markAsSuccess();
emitBuildSystemUpdated();
}
bool PythonBuildSystem::saveRawFileList(const QStringList &rawFileList)
{
const bool result = saveRawList(rawFileList, projectFilePath().toString());
// refresh(PythonProject::Files);
return result;
}
bool PythonBuildSystem::saveRawList(const QStringList &rawList, const QString &fileName)
{
const FilePath filePath = FilePath::fromString(fileName);
FileChangeBlocker changeGuarg(filePath);
bool result = false;
// New project file
if (fileName.endsWith(".pyproject")) {
FileSaver saver(filePath, QIODevice::ReadOnly | QIODevice::Text);
if (!saver.hasError()) {
QString content = QTextStream(saver.file()).readAll();
if (saver.finalize(ICore::dialogParent())) {
QString errorMessage;
result = writePyProjectFile(fileName, content, rawList, &errorMessage);
if (!errorMessage.isEmpty())
MessageManager::writeDisrupting(errorMessage);
}
}
} else { // Old project file
FileSaver saver(filePath, QIODevice::WriteOnly | QIODevice::Text);
if (!saver.hasError()) {
QTextStream stream(saver.file());
for (const QString &filePath : rawList)
stream << filePath << '\n';
saver.setResult(&stream);
result = saver.finalize(ICore::dialogParent());
}
}
return result;
}
bool PythonBuildSystem::writePyProjectFile(const QString &fileName, QString &content,
const QStringList &rawList, QString *errorMessage)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
*errorMessage = Tr::tr("Unable to open \"%1\" for reading: %2")
.arg(fileName, file.errorString());
return false;
}
// Build list of files with the current rawList for the JSON file
QString files("[");
for (const QString &f : rawList)
if (!f.endsWith(".pyproject"))
files += QString("\"%1\",").arg(f);
files = files.left(files.lastIndexOf(',')); // Removing leading comma
files += ']';
// Removing everything inside square parenthesis
// to replace it with the new list of files for the JSON file.
QRegularExpression pattern(R"(\[.*\])");
content.replace(pattern, files);
file.write(content.toUtf8());
return true;
}
bool PythonBuildSystem::addFiles(Node *, const FilePaths &filePaths, FilePaths *)
{
QStringList newList = m_rawFileList;
const QDir baseDir(projectDirectory().toString());
for (const FilePath &filePath : filePaths)
newList.append(baseDir.relativeFilePath(filePath.toString()));
return saveRawFileList(newList);
}
RemovedFilesFromProject PythonBuildSystem::removeFiles(Node *, const FilePaths &filePaths, FilePaths *)
{
QStringList newList = m_rawFileList;
for (const FilePath &filePath : filePaths) {
const QHash<QString, QString>::iterator i = m_rawListEntries.find(filePath.toString());
if (i != m_rawListEntries.end())
newList.removeOne(i.value());
}
bool res = saveRawFileList(newList);
return res ? RemovedFilesFromProject::Ok : RemovedFilesFromProject::Error;
}
bool PythonBuildSystem::deleteFiles(Node *, const FilePaths &)
{
return true;
}
bool PythonBuildSystem::renameFile(Node *, const FilePath &oldFilePath, const FilePath &newFilePath)
{
QStringList newList = m_rawFileList;
const QHash<QString, QString>::iterator i = m_rawListEntries.find(oldFilePath.toString());
if (i != m_rawListEntries.end()) {
const int index = newList.indexOf(i.value());
if (index != -1) {
const QDir baseDir(projectDirectory().toString());
newList.replace(index, baseDir.relativeFilePath(newFilePath.toString()));
}
}
return saveRawFileList(newList);
}
void PythonBuildSystem::parse()
{
m_rawListEntries.clear();
m_rawQmlImportPathEntries.clear();
const FilePath filePath = projectFilePath();
// The PySide project file is JSON based
if (filePath.endsWith(".pyproject")) {
QString errorMessage;
m_rawFileList = readLinesJson(filePath, &errorMessage);
if (!errorMessage.isEmpty())
MessageManager::writeFlashing(errorMessage);
errorMessage.clear();
m_rawQmlImportPathList = readImportPathsJson(filePath, &errorMessage);
if (!errorMessage.isEmpty())
MessageManager::writeFlashing(errorMessage);
} else if (filePath.endsWith(".pyqtc")) {
// To keep compatibility with PyQt we keep the compatibility with plain
// text files as project files.
m_rawFileList = readLines(filePath);
}
m_files = processEntries(m_rawFileList, &m_rawListEntries);
m_qmlImportPaths = processEntries(m_rawQmlImportPathList, &m_rawQmlImportPathEntries);
}
/**
* Expands environment variables in the given \a string when they are written
* like $$(VARIABLE).
*/
static void expandEnvironmentVariables(const QProcessEnvironment &env, QString &string)
{
const QRegularExpression candidate("\\$\\$\\((.+)\\)");
QRegularExpressionMatch match;
int index = string.indexOf(candidate, 0, &match);
while (index != -1) {
const QString value = env.value(match.captured(1));
string.replace(index, match.capturedLength(), value);
index += value.length();
index = string.indexOf(candidate, index, &match);
}
}
/**
* Expands environment variables and converts the path from relative to the
* project to an absolute path.
*
* The \a map variable is an optional argument that will map the returned
* absolute paths back to their original \a paths.
*/
QStringList PythonBuildSystem::processEntries(const QStringList &paths,
QHash<QString, QString> *map) const
{
const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
const QDir projectDir(projectDirectory().toString());
QFileInfo fileInfo;
QStringList absolutePaths;
for (const QString &path : paths) {
QString trimmedPath = path.trimmed();
if (trimmedPath.isEmpty())
continue;
expandEnvironmentVariables(env, trimmedPath);
trimmedPath = FilePath::fromUserInput(trimmedPath).toString();
fileInfo.setFile(projectDir, trimmedPath);
if (fileInfo.exists()) {
const QString absPath = fileInfo.absoluteFilePath();
absolutePaths.append(absPath);
if (map)
map->insert(absPath, trimmedPath);
}
}
absolutePaths.removeDuplicates();
return absolutePaths;
}
Project::RestoreResult PythonProject::fromMap(const QVariantMap &map, QString *errorMessage)
{
Project::RestoreResult res = Project::fromMap(map, errorMessage);
if (res == RestoreResult::Ok) {
if (!activeTarget())
addTargetForDefaultKit();
}
return res;
}
PythonBuildSystem::PythonBuildSystem(Target *target)
: BuildSystem(target)
{
connect(target->project(), &Project::projectFileIsDirty, this, [this] { triggerParsing(); });
QTimer::singleShot(0, this, &PythonBuildSystem::triggerParsing);
}
bool PythonBuildSystem::supportsAction(Node *context, ProjectAction action, const Node *node) const
{
if (node->asFileNode()) {
return action == ProjectAction::Rename
|| action == ProjectAction::RemoveFile;
}
if (node->isFolderNodeType() || node->isProjectNodeType()) {
return action == ProjectAction::AddNewFile
|| action == ProjectAction::RemoveFile
|| action == ProjectAction::AddExistingFile;
}
return BuildSystem::supportsAction(context, action, node);
}
} // Python::Internal