forked from qt-creator/qt-creator
Lua: Add Install module
Allows plugins to install packages they might need. Change-Id: I4948dd0a6568e093fc35e4486d2e2a084090e103 Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -10,6 +10,7 @@ add_qtc_plugin(Lua
|
||||
bindings/fetch.cpp
|
||||
bindings/hook.cpp
|
||||
bindings/inheritance.h
|
||||
bindings/install.cpp
|
||||
bindings/layout.cpp
|
||||
bindings/messagemanager.cpp
|
||||
bindings/qtcprocess.cpp
|
||||
|
||||
@@ -92,11 +92,12 @@ void addFetchModule()
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
callback(QString("%1 (%2)")
|
||||
callback(QString("%1 (%2):\n%3")
|
||||
.arg(reply->errorString())
|
||||
.arg(QString::fromLatin1(
|
||||
QMetaEnum::fromType<QNetworkReply::NetworkError>()
|
||||
.valueToKey(reply->error()))));
|
||||
.valueToKey(reply->error())))
|
||||
.arg(QString::fromUtf8(reply->readAll())));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
351
src/plugins/lua/bindings/install.cpp
Normal file
351
src/plugins/lua/bindings/install.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "../luaengine.h"
|
||||
#include "../luatr.h"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <coreplugin/progressmanager/progressmanager.h>
|
||||
#include <coreplugin/progressmanager/taskprogress.h>
|
||||
|
||||
#include <solutions/tasking/networkquery.h>
|
||||
#include <solutions/tasking/tasktree.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/infobar.h>
|
||||
#include <utils/networkaccessmanager.h>
|
||||
#include <utils/stylehelper.h>
|
||||
#include <utils/unarchiver.h>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QLabel>
|
||||
#include <QTemporaryFile>
|
||||
#include <QtConcurrent>
|
||||
|
||||
using namespace Core;
|
||||
using namespace Tasking;
|
||||
using namespace Utils;
|
||||
|
||||
namespace Lua::Internal {
|
||||
|
||||
expected_str<QJsonDocument> getPackageInfo(const FilePath &appDataPath)
|
||||
{
|
||||
const FilePath packageInfoPath = appDataPath / "package.json";
|
||||
|
||||
if (!packageInfoPath.exists())
|
||||
return QJsonDocument();
|
||||
|
||||
expected_str<QByteArray> json = packageInfoPath.fileContents();
|
||||
if (!json)
|
||||
return make_unexpected(json.error());
|
||||
|
||||
if (json->isEmpty())
|
||||
return QJsonDocument();
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc(QJsonDocument::fromJson(*json, &error));
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
return make_unexpected(error.errorString());
|
||||
|
||||
if (!doc.isObject())
|
||||
return make_unexpected(Tr::tr("Package info is not an object"));
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
expected_str<QJsonObject> getInstalledPackageInfo(const FilePath &appDataPath, const QString &name)
|
||||
{
|
||||
auto packageDoc = getPackageInfo(appDataPath);
|
||||
if (!packageDoc)
|
||||
return make_unexpected(packageDoc.error());
|
||||
|
||||
QJsonObject root = packageDoc->object();
|
||||
|
||||
if (root.contains(name)) {
|
||||
QJsonValue v = root[name];
|
||||
if (!v.isObject())
|
||||
return make_unexpected(Tr::tr("Installed package info is not an object"));
|
||||
return v.toObject();
|
||||
}
|
||||
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
expected_str<QJsonDocument> getOrCreatePackageInfo(const FilePath &appDataPath)
|
||||
{
|
||||
expected_str<QJsonDocument> doc = getPackageInfo(appDataPath);
|
||||
if (doc && doc->isObject())
|
||||
return doc;
|
||||
|
||||
QJsonObject obj;
|
||||
return QJsonDocument(obj);
|
||||
}
|
||||
|
||||
expected_str<void> savePackageInfo(const FilePath &appDataPath, const QJsonDocument &doc)
|
||||
{
|
||||
if (!appDataPath.ensureWritableDir())
|
||||
return make_unexpected(Tr::tr("Could not create app data directory"));
|
||||
|
||||
const FilePath packageInfoPath = appDataPath / "package.json";
|
||||
return packageInfoPath.writeFileContents(doc.toJson())
|
||||
.transform_error([](const QString &error) {
|
||||
return Tr::tr("Could not write to package info: %1").arg(error);
|
||||
})
|
||||
.transform([](qint64) { return; });
|
||||
}
|
||||
|
||||
struct InstallOptions
|
||||
{
|
||||
QUrl url;
|
||||
QString name;
|
||||
QString version;
|
||||
};
|
||||
|
||||
size_t qHash(const InstallOptions &item, size_t seed = 0)
|
||||
{
|
||||
return qHash(item.url, seed) ^ qHash(item.name, seed) ^ qHash(item.version, seed);
|
||||
}
|
||||
|
||||
static FilePath destination(const FilePath &appDataPath, const InstallOptions &options)
|
||||
{
|
||||
return appDataPath / "packages" / options.name / options.version;
|
||||
}
|
||||
|
||||
static Group installRecipe(
|
||||
const FilePath &appDataPath,
|
||||
const QList<InstallOptions> &installOptions,
|
||||
const sol::protected_function &callback)
|
||||
{
|
||||
Storage<QFile> storage;
|
||||
|
||||
const LoopList<InstallOptions> installOptionsIt(installOptions);
|
||||
|
||||
const auto emitResult = [callback](const QString &error = QString()) {
|
||||
if (error.isEmpty()) {
|
||||
LuaEngine::void_safe_call(callback, true);
|
||||
return DoneResult::Success;
|
||||
}
|
||||
LuaEngine::void_safe_call(callback, false, error);
|
||||
return DoneResult::Error;
|
||||
};
|
||||
|
||||
const auto onDownloadSetup = [installOptionsIt](NetworkQuery &query) {
|
||||
query.setRequest(QNetworkRequest(installOptionsIt->url));
|
||||
query.setNetworkAccessManager(NetworkAccessManager::instance());
|
||||
return SetupResult::Continue;
|
||||
};
|
||||
|
||||
const auto onDownloadDone = [emitResult, storage](const NetworkQuery &query, DoneWith result) {
|
||||
if (result == DoneWith::Error)
|
||||
return emitResult(query.reply()->errorString());
|
||||
if (result == DoneWith::Cancel)
|
||||
return DoneResult::Error;
|
||||
|
||||
QNetworkReply *reply = query.reply();
|
||||
const auto size = reply->size();
|
||||
const auto written = storage->write(reply->readAll());
|
||||
if (written != size)
|
||||
return emitResult(Tr::tr("Could not write to temporary file"));
|
||||
storage->close();
|
||||
return DoneResult::Success;
|
||||
};
|
||||
|
||||
const auto onUnarchiveSetup =
|
||||
[appDataPath, installOptionsIt, storage, emitResult](Unarchiver &unarchiver) {
|
||||
const auto sourceAndCommand = Unarchiver::sourceAndCommand(
|
||||
FilePath::fromUserInput(storage->fileName()));
|
||||
|
||||
if (!sourceAndCommand) {
|
||||
emitResult(sourceAndCommand.error());
|
||||
return SetupResult::StopWithError;
|
||||
}
|
||||
unarchiver.setSourceAndCommand(*sourceAndCommand);
|
||||
unarchiver.setDestDir(destination(appDataPath, *installOptionsIt));
|
||||
return SetupResult::Continue;
|
||||
};
|
||||
|
||||
const auto onUnarchiverDone = [appDataPath, installOptionsIt, emitResult](DoneWith result) {
|
||||
if (result == DoneWith::Error)
|
||||
return emitResult(Tr::tr("Unarchiving failed"));
|
||||
if (result == DoneWith::Cancel)
|
||||
return DoneResult::Error;
|
||||
|
||||
expected_str<QJsonDocument> doc = getOrCreatePackageInfo(appDataPath);
|
||||
if (!doc)
|
||||
return emitResult(doc.error());
|
||||
|
||||
QJsonObject obj = doc->object();
|
||||
QJsonObject installedPackage;
|
||||
installedPackage["version"] = installOptionsIt->version;
|
||||
installedPackage["name"] = installOptionsIt->name;
|
||||
installedPackage["path"] = destination(appDataPath, *installOptionsIt).toFSPathString();
|
||||
obj[installOptionsIt->name] = installedPackage;
|
||||
|
||||
expected_str<void> res = savePackageInfo(appDataPath, QJsonDocument(obj));
|
||||
if (!res)
|
||||
return emitResult(res.error());
|
||||
return DoneResult::Success;
|
||||
};
|
||||
|
||||
return Group{
|
||||
storage,
|
||||
parallelIdealThreadCountLimit,
|
||||
installOptionsIt,
|
||||
Group{
|
||||
onGroupSetup([emitResult, storage, installOptionsIt] {
|
||||
const QString fileName = installOptionsIt->url.fileName();
|
||||
const QString ext = fileName.mid(fileName.indexOf('.'));
|
||||
{
|
||||
QTemporaryFile tempFile(QDir::tempPath() + "/XXXXXX" + ext);
|
||||
tempFile.setAutoRemove(false);
|
||||
tempFile.open();
|
||||
(*storage).setFileName(tempFile.fileName());
|
||||
}
|
||||
|
||||
if (!storage->open(QIODevice::WriteOnly)) {
|
||||
emitResult(Tr::tr("Could not open temporary file"));
|
||||
return SetupResult::StopWithError;
|
||||
}
|
||||
return SetupResult::Continue;
|
||||
}),
|
||||
NetworkQueryTask(onDownloadSetup, onDownloadDone),
|
||||
UnarchiverTask(onUnarchiveSetup, onUnarchiverDone),
|
||||
onGroupDone([storage, emitResult] { storage->remove(); }),
|
||||
},
|
||||
onGroupDone([emitResult](DoneWith result) {
|
||||
if (result == DoneWith::Cancel)
|
||||
emitResult("Installation was canceled");
|
||||
else if (result == DoneWith::Success)
|
||||
emitResult();
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
void addInstallModule()
|
||||
{
|
||||
class State
|
||||
{
|
||||
public:
|
||||
State() = default;
|
||||
State(const State &) {}
|
||||
~State()
|
||||
{
|
||||
for (auto tree : m_trees)
|
||||
delete tree;
|
||||
}
|
||||
|
||||
TaskTree *createTree()
|
||||
{
|
||||
auto tree = new TaskTree();
|
||||
m_trees.append(tree);
|
||||
QObject::connect(tree, &TaskTree::done, tree, &QObject::deleteLater);
|
||||
return tree;
|
||||
};
|
||||
|
||||
private:
|
||||
QList<QPointer<TaskTree>> m_trees;
|
||||
};
|
||||
|
||||
LuaEngine::registerProvider(
|
||||
"Install", [state = State()](sol::state_view lua) mutable -> sol::object {
|
||||
sol::table async
|
||||
= lua.script("return require('async')", "_install_async_").get<sol::table>();
|
||||
sol::function wrap = async["wrap"];
|
||||
|
||||
sol::table install = lua.create_table();
|
||||
const ScriptPluginSpec pluginSpec = lua.get<ScriptPluginSpec>("PluginSpec");
|
||||
|
||||
install["packageInfo"] =
|
||||
[pluginSpec](const QString &name, sol::this_state l) -> sol::optional<sol::table> {
|
||||
expected_str<QJsonObject> obj
|
||||
= getInstalledPackageInfo(pluginSpec.appDataPath, name);
|
||||
if (!obj)
|
||||
throw sol::error(obj.error().toStdString());
|
||||
|
||||
return sol::table::create_with(
|
||||
l.lua_state(),
|
||||
"name",
|
||||
obj->value("name").toString(),
|
||||
"version",
|
||||
obj->value("version").toString(),
|
||||
"path",
|
||||
FilePath::fromUserInput(obj->value("path").toString()));
|
||||
};
|
||||
|
||||
install["install_cb"] =
|
||||
[pluginSpec, &state](
|
||||
const QString &msg,
|
||||
const sol::table &installOptions,
|
||||
const sol::function &callback) {
|
||||
QList<InstallOptions> installOptionsList;
|
||||
if (installOptions.size() > 0) {
|
||||
for (const auto &pair : installOptions) {
|
||||
const sol::object &value = pair.second;
|
||||
if (value.get_type() != sol::type::table)
|
||||
throw sol::error("Install options must be a table");
|
||||
const sol::table &table = value.as<sol::table>();
|
||||
const QString name = table["name"];
|
||||
const QUrl url = QUrl::fromUserInput(table["url"]);
|
||||
if (url.scheme() != "https")
|
||||
throw sol::error("Only HTTPS is supported");
|
||||
|
||||
const QString version = table["version"];
|
||||
installOptionsList.append({url, name, version});
|
||||
}
|
||||
} else {
|
||||
const QString name = installOptions["name"];
|
||||
const QUrl url = QUrl::fromUserInput(installOptions["url"]);
|
||||
const QString version = installOptions["version"];
|
||||
if (url.scheme() != "https")
|
||||
throw sol::error("Only HTTPS is supported");
|
||||
installOptionsList.append({url, name, version});
|
||||
}
|
||||
|
||||
const Utils::Id infoBarId = Utils::Id::fromString(
|
||||
"Install" + pluginSpec.name + QString::number(qHash(installOptionsList)));
|
||||
|
||||
InfoBarEntry entry(infoBarId, msg, InfoBarEntry::GlobalSuppression::Enabled);
|
||||
|
||||
entry.addCustomButton(
|
||||
Tr::tr("Install"),
|
||||
[infoBarId, &state, pluginSpec, installOptionsList, callback]() {
|
||||
auto tree = state.createTree();
|
||||
|
||||
auto progress = new TaskProgress(tree);
|
||||
progress->setDisplayName(Tr::tr("Installing package(s) %1").arg("..."));
|
||||
|
||||
tree->setRecipe(
|
||||
installRecipe(pluginSpec.appDataPath, installOptionsList, callback));
|
||||
tree->start();
|
||||
|
||||
Core::ICore::infoBar()->removeInfo(infoBarId);
|
||||
});
|
||||
entry.setCancelButtonInfo(
|
||||
[callback]() { callback(false, "User denied installation"); });
|
||||
|
||||
entry.setDetailsWidgetCreator([pluginSpec, installOptionsList]() -> QWidget * {
|
||||
const QString markdown
|
||||
= Tr::tr("The plugin \"**%1**\" would like to install the following "
|
||||
"package(s):\n\n")
|
||||
.arg(pluginSpec.name)
|
||||
+ Utils::transform(installOptionsList, [](const InstallOptions &options) {
|
||||
return QString("* %1 - %2 (from: [%3](%3))")
|
||||
.arg(options.name, options.version, options.url.toString());
|
||||
}).join("\n");
|
||||
|
||||
QLabel *list = new QLabel();
|
||||
list->setTextFormat(Qt::TextFormat::MarkdownText);
|
||||
list->setText(markdown);
|
||||
list->setMargin(StyleHelper::SpacingTokens::ExPaddingGapS);
|
||||
return list;
|
||||
});
|
||||
Core::ICore::infoBar()->addInfo(entry);
|
||||
};
|
||||
|
||||
install["install"] = wrap(install["install_cb"]);
|
||||
|
||||
return install;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Lua::Internal
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "luapluginspec.h"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <coreplugin/messagemanager.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
|
||||
using namespace Utils;
|
||||
|
||||
@@ -151,6 +153,16 @@ expected_str<void> LuaEngine::prepareSetup(
|
||||
const QString searchPath = (pluginSpec.location() / "?.lua").toUserOutput();
|
||||
lua["package"]["path"] = searchPath.toStdString();
|
||||
|
||||
const FilePath appDataPath = Core::ICore::userResourcePath() / "plugin-data" / "lua"
|
||||
/ pluginSpec.location().fileName();
|
||||
|
||||
sol::environment env(lua, sol::create, lua.globals());
|
||||
|
||||
lua.new_usertype<ScriptPluginSpec>(
|
||||
"PluginSpec", sol::no_constructor, "name", sol::readonly(&ScriptPluginSpec::name));
|
||||
|
||||
lua["PluginSpec"] = ScriptPluginSpec{pluginSpec.name(), appDataPath};
|
||||
|
||||
// TODO: only register what the plugin requested
|
||||
for (const auto &[name, func] : d->m_providers.asKeyValueRange()) {
|
||||
lua["package"]["preload"][name.toStdString()] = [func = func](const sol::this_state &s) {
|
||||
|
||||
@@ -33,6 +33,12 @@ struct CoroutineState
|
||||
bool isMainThread;
|
||||
};
|
||||
|
||||
struct ScriptPluginSpec
|
||||
{
|
||||
QString name;
|
||||
Utils::FilePath appDataPath;
|
||||
};
|
||||
|
||||
class LUA_EXPORT LuaEngine final : public QObject
|
||||
{
|
||||
friend class Internal::LuaPlugin;
|
||||
|
||||
@@ -32,6 +32,7 @@ void addLayoutModule();
|
||||
void addQtModule();
|
||||
void addCoreModule();
|
||||
void addHookModule();
|
||||
void addInstallModule();
|
||||
|
||||
class LuaJsExtension : public QObject
|
||||
{
|
||||
@@ -75,6 +76,7 @@ public:
|
||||
addQtModule();
|
||||
addCoreModule();
|
||||
addHookModule();
|
||||
addInstallModule();
|
||||
|
||||
Core::JsExpander::registerGlobalObject("Lua", [] { return new LuaJsExtension(); });
|
||||
}
|
||||
|
||||
29
src/plugins/lua/meta/install.lua
Normal file
29
src/plugins/lua/meta/install.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
---@meta Install
|
||||
|
||||
local Install = {}
|
||||
|
||||
---@class PackageInfo
|
||||
---@field name string The name of the package
|
||||
---@field version string The version of the package
|
||||
---@field path FilePath The path to the package
|
||||
|
||||
local PackageInfo = {}
|
||||
---@class InstallOptions
|
||||
---@field name string The name of the package to install
|
||||
---@field url string The url to fetch the package from
|
||||
---@field version string The version of the package to install
|
||||
local InstallOptions = {}
|
||||
|
||||
---Install something
|
||||
---@param msg string The message to display to the user asking for permission to install
|
||||
---@param options InstallOptions|[InstallOptions] The options to install
|
||||
---@return boolean Result Whether the installation was successful
|
||||
---@return string Error The error message if the installation failed.
|
||||
function Install.install(msg, options) end
|
||||
|
||||
---Get the package info
|
||||
---@param name any The name of the package
|
||||
---@return PackageInfo
|
||||
function Install.packageInfo(name) end
|
||||
|
||||
return Install
|
||||
Reference in New Issue
Block a user