533 lines
17 KiB
C
533 lines
17 KiB
C
|
/* ide-application-plugins.c
|
||
|
*
|
||
|
* Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
|
||
|
*
|
||
|
* This program is free software: you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License as published by
|
||
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
* (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*
|
||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
*/
|
||
|
|
||
|
#define G_LOG_DOMAIN "ide-application-plugins"
|
||
|
|
||
|
#include "config.h"
|
||
|
|
||
|
#include <libide-plugins.h>
|
||
|
|
||
|
#include "ide-application.h"
|
||
|
#include "ide-application-addin.h"
|
||
|
#include "ide-application-private.h"
|
||
|
|
||
|
static void
|
||
|
ide_application_changed_plugin_cb (GSettings *settings,
|
||
|
const gchar *key,
|
||
|
PeasPluginInfo *plugin_info)
|
||
|
{
|
||
|
PeasEngine *engine;
|
||
|
|
||
|
IDE_ENTRY;
|
||
|
|
||
|
g_assert (G_IS_SETTINGS (settings));
|
||
|
g_assert (key != NULL);
|
||
|
g_assert (plugin_info != NULL);
|
||
|
|
||
|
engine = peas_engine_get_default ();
|
||
|
|
||
|
if (!g_settings_get_boolean (settings, key))
|
||
|
peas_engine_unload_plugin (engine, plugin_info);
|
||
|
else
|
||
|
peas_engine_load_plugin (engine, plugin_info);
|
||
|
|
||
|
IDE_EXIT;
|
||
|
}
|
||
|
|
||
|
static GSettings *
|
||
|
_ide_application_plugin_get_settings (IdeApplication *self,
|
||
|
PeasPluginInfo *plugin_info)
|
||
|
{
|
||
|
GSettings *settings;
|
||
|
const gchar *module_name;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
|
||
|
module_name = peas_plugin_info_get_module_name (plugin_info);
|
||
|
|
||
|
if G_UNLIKELY (self->plugin_settings == NULL)
|
||
|
self->plugin_settings =
|
||
|
g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
|
||
|
|
||
|
if (!(settings = g_hash_table_lookup (self->plugin_settings, module_name)))
|
||
|
{
|
||
|
g_autofree gchar *path = NULL;
|
||
|
|
||
|
path = g_strdup_printf ("/org/gnome/builder/plugins/%s/", module_name);
|
||
|
settings = g_settings_new_with_path ("org.gnome.builder.plugin", path);
|
||
|
g_hash_table_insert (self->plugin_settings, g_strdup (module_name), settings);
|
||
|
|
||
|
g_signal_connect (settings,
|
||
|
"changed::enabled",
|
||
|
G_CALLBACK (ide_application_changed_plugin_cb),
|
||
|
plugin_info);
|
||
|
}
|
||
|
|
||
|
return settings;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
ide_application_can_load_plugin (IdeApplication *self,
|
||
|
PeasPluginInfo *plugin_info,
|
||
|
GHashTable *circular)
|
||
|
{
|
||
|
PeasEngine *engine = peas_engine_get_default ();
|
||
|
const gchar *module_name;
|
||
|
const gchar *module_dir;
|
||
|
const gchar **deps;
|
||
|
GSettings *settings;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (circular != NULL);
|
||
|
|
||
|
if (plugin_info == NULL)
|
||
|
return FALSE;
|
||
|
|
||
|
module_dir = peas_plugin_info_get_module_dir (plugin_info);
|
||
|
module_name = peas_plugin_info_get_module_name (plugin_info);
|
||
|
|
||
|
/* Short-circuit for single-plugin mode */
|
||
|
if (self->plugin != NULL)
|
||
|
return ide_str_equal0 (module_name, self->plugin);
|
||
|
|
||
|
if (g_hash_table_contains (circular, module_name))
|
||
|
{
|
||
|
g_warning ("Circular dependency found in module %s", module_name);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
g_hash_table_add (circular, (gpointer)module_name);
|
||
|
|
||
|
/* Make sure the plugin has not been disabled in settings. */
|
||
|
settings = _ide_application_plugin_get_settings (self, plugin_info);
|
||
|
if (!g_settings_get_boolean (settings, "enabled"))
|
||
|
return FALSE;
|
||
|
|
||
|
#if 0
|
||
|
if (self->mode == IDE_APPLICATION_MODE_WORKER)
|
||
|
{
|
||
|
if (self->worker != plugin_info)
|
||
|
return FALSE;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
/*
|
||
|
* If the plugin is not bundled within the Builder executable, then we
|
||
|
* require that an X-Builder-ABI=major.minor style extended data be
|
||
|
* provided to ensure we have proper ABI.
|
||
|
*
|
||
|
* You could get around this by loading a plugin that then loads resouces
|
||
|
* containing external data, but this is good enough for now.
|
||
|
*/
|
||
|
|
||
|
if (!g_str_has_prefix (module_dir, "resource:///plugins/"))
|
||
|
{
|
||
|
const gchar *abi;
|
||
|
|
||
|
if (!(abi = peas_plugin_info_get_external_data (plugin_info, "Builder-ABI")))
|
||
|
{
|
||
|
g_critical ("Refusing to load plugin %s because X-Builder-ABI is missing",
|
||
|
module_name);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
if (g_strcmp0 (PACKAGE_ABI_S, abi) != 0)
|
||
|
{
|
||
|
g_critical ("Refusing to load plugin %s, expected ABI %d.%d and got %s",
|
||
|
module_name, IDE_MAJOR_VERSION, 0, abi);
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* If this plugin has dependencies, we need to check that the dependencies
|
||
|
* can also be loaded.
|
||
|
*/
|
||
|
if ((deps = peas_plugin_info_get_dependencies (plugin_info)))
|
||
|
{
|
||
|
for (guint i = 0; deps[i]; i++)
|
||
|
{
|
||
|
PeasPluginInfo *dep = peas_engine_get_plugin_info (engine, deps[i]);
|
||
|
|
||
|
if (!ide_application_can_load_plugin (self, dep, circular))
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
g_hash_table_remove (circular, (gpointer)module_name);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ide_application_load_plugin_resources (IdeApplication *self,
|
||
|
PeasEngine *engine,
|
||
|
PeasPluginInfo *plugin_info)
|
||
|
{
|
||
|
g_autofree gchar *gresources_path = NULL;
|
||
|
g_autofree gchar *gresources_basename = NULL;
|
||
|
const gchar *module_dir;
|
||
|
const gchar *module_name;
|
||
|
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
g_assert (PEAS_IS_ENGINE (engine));
|
||
|
|
||
|
module_dir = peas_plugin_info_get_module_dir (plugin_info);
|
||
|
module_name = peas_plugin_info_get_module_name (plugin_info);
|
||
|
gresources_basename = g_strdup_printf ("%s.gresource", module_name);
|
||
|
gresources_path = g_build_filename (module_dir, gresources_basename, NULL);
|
||
|
|
||
|
if (g_file_test (gresources_path, G_FILE_TEST_IS_REGULAR))
|
||
|
{
|
||
|
g_autofree gchar *resource_path = NULL;
|
||
|
g_autoptr(GError) error = NULL;
|
||
|
GResource *resource;
|
||
|
|
||
|
resource = g_resource_load (gresources_path, &error);
|
||
|
|
||
|
if (resource == NULL)
|
||
|
{
|
||
|
g_warning ("Failed to load gresources: %s", error->message);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
g_hash_table_insert (self->plugin_gresources, g_strdup (module_name), resource);
|
||
|
g_resources_register (resource);
|
||
|
|
||
|
resource_path = g_strdup_printf ("resource:///plugins/%s", module_name);
|
||
|
dzl_application_add_resources (DZL_APPLICATION (self), resource_path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void
|
||
|
_ide_application_load_plugin (IdeApplication *self,
|
||
|
PeasPluginInfo *plugin_info)
|
||
|
{
|
||
|
PeasEngine *engine = peas_engine_get_default ();
|
||
|
g_autoptr(GHashTable) circular = NULL;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
|
||
|
circular = g_hash_table_new (g_str_hash, g_str_equal);
|
||
|
|
||
|
if (ide_application_can_load_plugin (self, plugin_info, circular))
|
||
|
peas_engine_load_plugin (engine, plugin_info);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ide_application_plugins_load_plugin_cb (IdeApplication *self,
|
||
|
PeasPluginInfo *plugin_info,
|
||
|
PeasEngine *engine)
|
||
|
{
|
||
|
const gchar *data_dir;
|
||
|
const gchar *module_dir;
|
||
|
const gchar *module_name;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
g_assert (PEAS_IS_ENGINE (engine));
|
||
|
|
||
|
data_dir = peas_plugin_info_get_data_dir (plugin_info);
|
||
|
module_dir = peas_plugin_info_get_module_dir (plugin_info);
|
||
|
module_name = peas_plugin_info_get_module_name (plugin_info);
|
||
|
|
||
|
g_debug ("Loaded plugin \"%s\" with module-dir \"%s\"",
|
||
|
module_name, module_dir);
|
||
|
|
||
|
if (peas_plugin_info_get_external_data (plugin_info, "Has-Resources"))
|
||
|
{
|
||
|
/* Possibly load bundled .gresource files if the plugin is not
|
||
|
* embedded into the application (such as python3 modules).
|
||
|
*/
|
||
|
ide_application_load_plugin_resources (self, engine, plugin_info);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Only register resources if the path is to an embedded resource
|
||
|
* or if it's not builtin (and therefore maybe doesn't use .gresource
|
||
|
* files). That helps reduce the number IOPS we do.
|
||
|
*/
|
||
|
if (g_str_has_prefix (data_dir, "resource://") ||
|
||
|
!peas_plugin_info_is_builtin (plugin_info))
|
||
|
dzl_application_add_resources (DZL_APPLICATION (self), data_dir);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ide_application_plugins_unload_plugin_cb (IdeApplication *self,
|
||
|
PeasPluginInfo *plugin_info,
|
||
|
PeasEngine *engine)
|
||
|
{
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
g_assert (PEAS_IS_ENGINE (engine));
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* _ide_application_load_plugins_for_startup:
|
||
|
*
|
||
|
* This function will load all of the plugins that are candidates for
|
||
|
* early-stage initialization. Usually, that is any plugin that has a
|
||
|
* command-line handler and uses "X-At-Startup=true" in their .plugin
|
||
|
* manifest.
|
||
|
*
|
||
|
* Since: 3.32
|
||
|
*/
|
||
|
void
|
||
|
_ide_application_load_plugins_for_startup (IdeApplication *self)
|
||
|
{
|
||
|
PeasEngine *engine = peas_engine_get_default ();
|
||
|
const GList *plugins;
|
||
|
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
|
||
|
g_signal_connect_object (engine,
|
||
|
"load-plugin",
|
||
|
G_CALLBACK (ide_application_plugins_load_plugin_cb),
|
||
|
self,
|
||
|
G_CONNECT_SWAPPED);
|
||
|
|
||
|
g_signal_connect_object (engine,
|
||
|
"unload-plugin",
|
||
|
G_CALLBACK (ide_application_plugins_unload_plugin_cb),
|
||
|
self,
|
||
|
G_CONNECT_SWAPPED);
|
||
|
|
||
|
/* Ensure that our embedded plugins are allowed early access to
|
||
|
* start loading (before we ever look at anything on disk). This
|
||
|
* ensures that only embedded plugins can be used at startup,
|
||
|
* saving us some precious disk I/O.
|
||
|
*/
|
||
|
peas_engine_prepend_search_path (engine, "resource:///plugins", "resource:///plugins");
|
||
|
|
||
|
/* If we are within the Flatpak, then load any extensions we've
|
||
|
* found merged into the extensions directory.
|
||
|
*/
|
||
|
if (ide_is_flatpak ())
|
||
|
peas_engine_add_search_path (engine,
|
||
|
"/app/extensions/lib/gnome-builder/plugins",
|
||
|
"/app/extensions/lib/gnome-builder/plugins");
|
||
|
|
||
|
/* Our first step is to load our "At-Startup" plugins, which may
|
||
|
* contain things like command-line handlers. For example, the
|
||
|
* greeter may handle command-line options and then show the
|
||
|
* greeter workspace.
|
||
|
*/
|
||
|
plugins = peas_engine_get_plugin_list (engine);
|
||
|
for (const GList *iter = plugins; iter; iter = iter->next)
|
||
|
{
|
||
|
PeasPluginInfo *plugin_info = iter->data;
|
||
|
|
||
|
if (!peas_plugin_info_is_loaded (plugin_info) &&
|
||
|
peas_plugin_info_get_external_data (plugin_info, "At-Startup"))
|
||
|
_ide_application_load_plugin (self, plugin_info);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* _ide_application_load_plugins:
|
||
|
* @self: a #IdeApplication
|
||
|
*
|
||
|
* This function loads any additional plugins that have not yet been
|
||
|
* loaded during early startup.
|
||
|
*
|
||
|
* Since: 3.32
|
||
|
*/
|
||
|
void
|
||
|
_ide_application_load_plugins (IdeApplication *self)
|
||
|
{
|
||
|
g_autofree gchar *user_plugins_dir = NULL;
|
||
|
g_autoptr(GError) error = NULL;
|
||
|
const GList *plugins;
|
||
|
PeasEngine *engine;
|
||
|
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
|
||
|
engine = peas_engine_get_default ();
|
||
|
|
||
|
/* Now that we have gotten past our startup plugins (which must be
|
||
|
* embedded into the gnome-builder executable, we can enable the
|
||
|
* system plugins that are loaded from disk.
|
||
|
*/
|
||
|
peas_engine_prepend_search_path (engine,
|
||
|
PACKAGE_LIBDIR"/gnome-builder/plugins",
|
||
|
PACKAGE_DATADIR"/gnome-builder/plugins");
|
||
|
|
||
|
if (ide_is_flatpak ())
|
||
|
{
|
||
|
g_autofree gchar *extensions_plugins_dir = NULL;
|
||
|
g_autofree gchar *plugins_dir = NULL;
|
||
|
|
||
|
plugins_dir = g_build_filename (g_get_home_dir (),
|
||
|
".local",
|
||
|
"share",
|
||
|
"gnome-builder",
|
||
|
"plugins",
|
||
|
NULL);
|
||
|
peas_engine_prepend_search_path (engine, plugins_dir, plugins_dir);
|
||
|
|
||
|
extensions_plugins_dir = g_build_filename ("/app",
|
||
|
"extensions",
|
||
|
"lib",
|
||
|
"gnome-builder",
|
||
|
"plugins",
|
||
|
NULL);
|
||
|
peas_engine_prepend_search_path (engine, extensions_plugins_dir, extensions_plugins_dir);
|
||
|
}
|
||
|
|
||
|
user_plugins_dir = g_build_filename (g_get_user_data_dir (),
|
||
|
"gnome-builder",
|
||
|
"plugins",
|
||
|
NULL);
|
||
|
peas_engine_prepend_search_path (engine, user_plugins_dir, NULL);
|
||
|
|
||
|
/* Ensure that we have all our required GObject Introspection packages
|
||
|
* loaded so that plugins don't need to require_version() as that is
|
||
|
* tedious and annoying to keep up to date.
|
||
|
*
|
||
|
* If we can't load any of our dependent packages, then fail to load
|
||
|
* python3 plugins altogether to avoid loading anything improper into
|
||
|
* the process space.
|
||
|
*/
|
||
|
g_irepository_prepend_search_path (PACKAGE_LIBDIR"/gnome-builder/girepository-1.0");
|
||
|
if (!g_irepository_require (NULL, "GtkSource", "4", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "Gio", "2.0", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "GLib", "2.0", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "Gtk", "3.0", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "Dazzle", "1.0", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "Jsonrpc", "1.0", 0, &error) ||
|
||
|
!g_irepository_require (NULL, "Template", "1.0", 0, &error) ||
|
||
|
#ifdef HAVE_WEBKIT
|
||
|
!g_irepository_require (NULL, "WebKit2", "4.0", 0, &error) ||
|
||
|
#endif
|
||
|
!g_irepository_require (NULL, "Ide", PACKAGE_ABI_S, 0, &error))
|
||
|
g_critical ("Cannot enable Python 3 plugins: %s", error->message);
|
||
|
else
|
||
|
peas_engine_enable_loader (engine, "python3");
|
||
|
|
||
|
peas_engine_rescan_plugins (engine);
|
||
|
|
||
|
plugins = peas_engine_get_plugin_list (engine);
|
||
|
|
||
|
for (const GList *iter = plugins; iter; iter = iter->next)
|
||
|
{
|
||
|
PeasPluginInfo *plugin_info = iter->data;
|
||
|
|
||
|
if (!peas_plugin_info_is_loaded (plugin_info))
|
||
|
_ide_application_load_plugin (self, plugin_info);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ide_application_addin_added_cb (PeasExtensionSet *set,
|
||
|
PeasPluginInfo *plugin_info,
|
||
|
PeasExtension *exten,
|
||
|
gpointer user_data)
|
||
|
{
|
||
|
IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
|
||
|
IdeApplication *self = user_data;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (PEAS_IS_EXTENSION_SET (set));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
g_assert (IDE_IS_APPLICATION_ADDIN (addin));
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
|
||
|
ide_application_addin_load (addin, self);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ide_application_addin_removed_cb (PeasExtensionSet *set,
|
||
|
PeasPluginInfo *plugin_info,
|
||
|
PeasExtension *exten,
|
||
|
gpointer user_data)
|
||
|
{
|
||
|
IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
|
||
|
IdeApplication *self = user_data;
|
||
|
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (PEAS_IS_EXTENSION_SET (set));
|
||
|
g_assert (plugin_info != NULL);
|
||
|
g_assert (IDE_IS_APPLICATION_ADDIN (addin));
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
|
||
|
ide_application_addin_unload (addin, self);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* _ide_application_load_addins:
|
||
|
* @self: a #IdeApplication
|
||
|
*
|
||
|
* Loads the #IdeApplicationAddin's for this application.
|
||
|
*
|
||
|
* Since: 3.32
|
||
|
*/
|
||
|
void
|
||
|
_ide_application_load_addins (IdeApplication *self)
|
||
|
{
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (self->addins == NULL);
|
||
|
|
||
|
self->addins = peas_extension_set_new (peas_engine_get_default (),
|
||
|
IDE_TYPE_APPLICATION_ADDIN,
|
||
|
NULL);
|
||
|
|
||
|
g_signal_connect (self->addins,
|
||
|
"extension-added",
|
||
|
G_CALLBACK (ide_application_addin_added_cb),
|
||
|
self);
|
||
|
|
||
|
g_signal_connect (self->addins,
|
||
|
"extension-removed",
|
||
|
G_CALLBACK (ide_application_addin_removed_cb),
|
||
|
self);
|
||
|
|
||
|
peas_extension_set_foreach (self->addins,
|
||
|
ide_application_addin_added_cb,
|
||
|
self);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* _ide_application_unload_addins:
|
||
|
* @self: a #IdeApplication
|
||
|
*
|
||
|
* Unloads all of the previously loaded #IdeApplicationAddin.
|
||
|
*
|
||
|
* Since: 3.32
|
||
|
*/
|
||
|
void
|
||
|
_ide_application_unload_addins (IdeApplication *self)
|
||
|
{
|
||
|
g_assert (IDE_IS_MAIN_THREAD ());
|
||
|
g_assert (IDE_IS_APPLICATION (self));
|
||
|
g_assert (self->addins != NULL);
|
||
|
|
||
|
g_clear_object (&self->addins);
|
||
|
}
|