summaryrefslogtreecommitdiffstats
path: root/plugins/core
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/core')
-rw-r--r--plugins/core/gs-plugin-appstream.c1653
-rw-r--r--plugins/core/gs-plugin-appstream.h22
-rw-r--r--plugins/core/gs-plugin-generic-updates.c148
-rw-r--r--plugins/core/gs-plugin-generic-updates.h22
-rw-r--r--plugins/core/gs-plugin-hardcoded-blocklist.c121
-rw-r--r--plugins/core/gs-plugin-hardcoded-blocklist.h22
-rw-r--r--plugins/core/gs-plugin-icons.c249
-rw-r--r--plugins/core/gs-plugin-icons.h22
-rw-r--r--plugins/core/gs-plugin-os-release.c180
-rw-r--r--plugins/core/gs-plugin-os-release.h22
-rw-r--r--plugins/core/gs-plugin-provenance-license.c199
-rw-r--r--plugins/core/gs-plugin-provenance-license.h22
-rw-r--r--plugins/core/gs-plugin-provenance.c292
-rw-r--r--plugins/core/gs-plugin-provenance.h22
-rw-r--r--plugins/core/gs-plugin-rewrite-resource.c252
-rw-r--r--plugins/core/gs-plugin-rewrite-resource.h22
-rw-r--r--plugins/core/gs-self-test.c277
-rw-r--r--plugins/core/meson.build133
l---------plugins/core/tests/os-release1
19 files changed, 3681 insertions, 0 deletions
diff --git a/plugins/core/gs-plugin-appstream.c b/plugins/core/gs-plugin-appstream.c
new file mode 100644
index 0000000..098e24a
--- /dev/null
+++ b/plugins/core/gs-plugin-appstream.c
@@ -0,0 +1,1653 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2019 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <errno.h>
+#include <gnome-software.h>
+#include <xmlb.h>
+
+#include "gs-appstream.h"
+#include "gs-external-appstream-utils.h"
+#include "gs-plugin-appstream.h"
+
+/*
+ * SECTION:
+ * Uses offline AppStream data to populate and refine package results.
+ *
+ * This plugin calls UpdatesChanged() if any of the AppStream stores are
+ * changed in any way.
+ *
+ * Methods: | AddCategory
+ * Refines: | [source]->[name,summary,pixbuf,id,kind]
+ */
+
+struct _GsPluginAppstream
+{
+ GsPlugin parent;
+
+ GsWorkerThread *worker; /* (owned) */
+
+ XbSilo *silo;
+ GRWLock silo_lock;
+ GSettings *settings;
+};
+
+G_DEFINE_TYPE (GsPluginAppstream, gs_plugin_appstream, GS_TYPE_PLUGIN)
+
+#define assert_in_worker(self) \
+ g_assert (gs_worker_thread_is_in_worker_context (self->worker))
+
+static void
+gs_plugin_appstream_dispose (GObject *object)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (object);
+
+ g_clear_object (&self->silo);
+ g_clear_object (&self->settings);
+ g_rw_lock_clear (&self->silo_lock);
+ g_clear_object (&self->worker);
+
+ G_OBJECT_CLASS (gs_plugin_appstream_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_appstream_init (GsPluginAppstream *self)
+{
+ GApplication *application = g_application_get_default ();
+
+ /* XbSilo needs external locking as we destroy the silo and build a new
+ * one when something changes */
+ g_rw_lock_init (&self->silo_lock);
+
+ /* need package name */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "dpkg");
+
+ /* require settings */
+ self->settings = g_settings_new ("org.gnome.software");
+
+ /* Can be NULL when running the self tests */
+ if (application) {
+ g_signal_connect_object (application, "repository-changed",
+ G_CALLBACK (gs_plugin_update_cache_state_for_repository), self, G_CONNECT_SWAPPED);
+ }
+}
+
+static const gchar *
+gs_plugin_appstream_convert_component_kind (const gchar *kind)
+{
+ if (g_strcmp0 (kind, "webapp") == 0)
+ return "web-application";
+ if (g_strcmp0 (kind, "desktop") == 0)
+ return "desktop-application";
+ return kind;
+}
+
+static gboolean
+gs_plugin_appstream_upgrade_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "application") == 0) {
+ g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL);
+ g_autofree gchar *kind = NULL;
+ if (id != NULL) {
+ kind = g_strdup (xb_builder_node_get_attr (id, "type"));
+ xb_builder_node_remove_attr (id, "type");
+ }
+ if (kind != NULL)
+ xb_builder_node_set_attr (bn, "type", kind);
+ xb_builder_node_set_element (bn, "component");
+ } else if (g_strcmp0 (xb_builder_node_get_element (bn), "metadata") == 0) {
+ xb_builder_node_set_element (bn, "custom");
+ } else if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ const gchar *type_old = xb_builder_node_get_attr (bn, "type");
+ const gchar *type_new = gs_plugin_appstream_convert_component_kind (type_old);
+ if (type_old != type_new)
+ xb_builder_node_set_attr (bn, "type", type_new);
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_add_icons_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") != 0)
+ return TRUE;
+ gs_appstream_component_add_extra_info (bn);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_add_origin_keyword_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) {
+ const gchar *origin = xb_builder_node_get_attr (bn, "origin");
+ GPtrArray *components = xb_builder_node_get_children (bn);
+ if (origin == NULL || origin[0] == '\0')
+ return TRUE;
+ g_debug ("origin %s has %u components", origin, components->len);
+ if (components->len < 200) {
+ for (guint i = 0; i < components->len; i++) {
+ XbBuilderNode *component = g_ptr_array_index (components, i);
+ gs_appstream_component_add_keyword (component, origin);
+ }
+ }
+ }
+ return TRUE;
+}
+
+static void
+gs_plugin_appstream_media_baseurl_free (gpointer user_data)
+{
+ g_string_free ((GString *) user_data, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_media_baseurl_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ GString *baseurl = user_data;
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) {
+ const gchar *url = xb_builder_node_get_attr (bn, "media_baseurl");
+ if (url == NULL) {
+ g_string_truncate (baseurl, 0);
+ return TRUE;
+ }
+ g_string_assign (baseurl, url);
+ return TRUE;
+ }
+
+ if (baseurl->len == 0)
+ return TRUE;
+
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "icon") == 0) {
+ const gchar *type = xb_builder_node_get_attr (bn, "type");
+ if (g_strcmp0 (type, "remote") != 0)
+ return TRUE;
+ gs_appstream_component_fix_url (bn, baseurl->str);
+ } else if (g_strcmp0 (xb_builder_node_get_element (bn), "screenshots") == 0) {
+ GPtrArray *screenshots = xb_builder_node_get_children (bn);
+ for (guint i = 0; i < screenshots->len; i++) {
+ XbBuilderNode *screenshot = g_ptr_array_index (screenshots, i);
+ GPtrArray *children = NULL;
+ /* Type-check for security */
+ if (g_strcmp0 (xb_builder_node_get_element (screenshot), "screenshot") != 0) {
+ continue;
+ }
+ children = xb_builder_node_get_children (screenshot);
+ for (guint j = 0; j < children->len; j++) {
+ XbBuilderNode *child = g_ptr_array_index (children, j);
+ const gchar *element = xb_builder_node_get_element (child);
+ if (g_strcmp0 (element, "image") != 0 &&
+ g_strcmp0 (element, "video") != 0)
+ continue;
+ gs_appstream_component_fix_url (child, baseurl->str);
+ }
+ }
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_load_appdata_fn (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GFile) file = g_file_new_for_path (filename);
+ g_autoptr(XbBuilderFixup) fixup = NULL;
+ g_autoptr(XbBuilderNode) info = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+
+ /* add source */
+ if (!xb_builder_source_load_file (source, file,
+#if LIBXMLB_CHECK_VERSION(0, 2, 0)
+ XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY,
+#else
+ XB_BUILDER_SOURCE_FLAG_WATCH_FILE,
+#endif
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+
+ /* fix up any legacy installed files */
+ fixup = xb_builder_fixup_new ("AppStreamUpgrade2",
+ gs_plugin_appstream_upgrade_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup, 3);
+ xb_builder_source_add_fixup (source, fixup);
+
+ /* add metadata */
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "filename", filename, NULL);
+ xb_builder_source_set_info (source, info);
+
+ /* success */
+ xb_builder_import_source (builder, source);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_load_appdata (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *path,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *fn;
+ g_autoptr(GDir) dir = NULL;
+ g_autoptr(GFile) parent = g_file_new_for_path (path);
+ if (!g_file_query_exists (parent, cancellable)) {
+ g_debug ("appstream: Skipping appdata path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist");
+ return TRUE;
+ }
+
+ g_debug ("appstream: Loading appdata path '%s'", path);
+
+ dir = g_dir_open (path, 0, error);
+ if (dir == NULL)
+ return FALSE;
+
+ while ((fn = g_dir_read_name (dir)) != NULL) {
+ if (g_str_has_suffix (fn, ".appdata.xml") ||
+ g_str_has_suffix (fn, ".metainfo.xml")) {
+ g_autofree gchar *filename = g_build_filename (path, fn, NULL);
+ g_autoptr(GError) error_local = NULL;
+ if (!gs_plugin_appstream_load_appdata_fn (self,
+ builder,
+ filename,
+ cancellable,
+ &error_local)) {
+ g_debug ("ignoring %s: %s", filename, error_local->message);
+ continue;
+ }
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static GInputStream *
+gs_plugin_appstream_load_desktop_cb (XbBuilderSource *self,
+ XbBuilderSourceCtx *ctx,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *xml = NULL;
+ g_autoptr(AsComponent) cpt = as_component_new ();
+ g_autoptr(AsContext) actx = as_context_new ();
+ g_autoptr(GBytes) bytes = NULL;
+ gboolean ret;
+
+ bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error);
+ if (bytes == NULL)
+ return NULL;
+
+ as_component_set_id (cpt, xb_builder_source_ctx_get_filename (ctx));
+ ret = as_component_load_from_bytes (cpt,
+ actx,
+ AS_FORMAT_KIND_DESKTOP_ENTRY,
+ bytes,
+ error);
+ if (!ret)
+ return NULL;
+ xml = as_component_to_xml_data (cpt, actx, error);
+ if (xml == NULL)
+ return NULL;
+ return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free);
+}
+
+static gboolean
+gs_plugin_appstream_load_desktop_fn (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GFile) file = g_file_new_for_path (filename);
+ g_autoptr(XbBuilderNode) info = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+
+ /* add support for desktop files */
+ xb_builder_source_add_adapter (source, "application/x-desktop",
+ gs_plugin_appstream_load_desktop_cb, NULL, NULL);
+
+ /* add source */
+ if (!xb_builder_source_load_file (source, file,
+#if LIBXMLB_CHECK_VERSION(0, 2, 0)
+ XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY,
+#else
+ XB_BUILDER_SOURCE_FLAG_WATCH_FILE,
+#endif
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+
+ /* add metadata */
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "filename", filename, NULL);
+ xb_builder_source_set_info (source, info);
+
+ /* success */
+ xb_builder_import_source (builder, source);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_load_desktop (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *path,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *fn;
+ g_autoptr(GDir) dir = NULL;
+ g_autoptr(GFile) parent = g_file_new_for_path (path);
+ if (!g_file_query_exists (parent, cancellable)) {
+ g_debug ("appstream: Skipping desktop path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist");
+ return TRUE;
+ }
+
+ g_debug ("appstream: Loading desktop path '%s'", path);
+
+ dir = g_dir_open (path, 0, error);
+ if (dir == NULL)
+ return FALSE;
+
+ while ((fn = g_dir_read_name (dir)) != NULL) {
+ if (g_str_has_suffix (fn, ".desktop")) {
+ g_autofree gchar *filename = g_build_filename (path, fn, NULL);
+ g_autoptr(GError) error_local = NULL;
+ if (g_strcmp0 (fn, "mimeinfo.cache") == 0)
+ continue;
+ if (!gs_plugin_appstream_load_desktop_fn (self,
+ builder,
+ filename,
+ cancellable,
+ &error_local)) {
+ g_debug ("ignoring %s: %s", filename, error_local->message);
+ continue;
+ }
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static GInputStream *
+gs_plugin_appstream_load_dep11_cb (XbBuilderSource *self,
+ XbBuilderSourceCtx *ctx,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(AsMetadata) mdata = as_metadata_new ();
+ g_autoptr(GBytes) bytes = NULL;
+ g_autoptr(GError) tmp_error = NULL;
+ g_autofree gchar *xml = NULL;
+
+ bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error);
+ if (bytes == NULL)
+ return NULL;
+
+ as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_COLLECTION);
+ as_metadata_parse_bytes (mdata,
+ bytes,
+ AS_FORMAT_KIND_YAML,
+ &tmp_error);
+ if (tmp_error != NULL) {
+ g_propagate_error (error, g_steal_pointer (&tmp_error));
+ return NULL;
+ }
+
+ xml = as_metadata_components_to_collection (mdata, AS_FORMAT_KIND_XML, &tmp_error);
+ if (xml == NULL) {
+ // This API currently returns NULL if there is nothing to serialize, so we
+ // have to test if this is an error or not.
+ // See https://gitlab.gnome.org/GNOME/gnome-software/-/merge_requests/763
+ // for discussion about changing this API.
+ if (tmp_error != NULL) {
+ g_propagate_error (error, g_steal_pointer (&tmp_error));
+ return NULL;
+ }
+
+ xml = g_strdup("");
+ }
+
+ return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free);
+}
+
+#if LIBXMLB_CHECK_VERSION(0,3,1)
+static gboolean
+gs_plugin_appstream_tokenize_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const gchar * const elements_to_tokenize[] = {
+ "id",
+ "keyword",
+ "launchable",
+ "mimetype",
+ "name",
+ "pkgname",
+ "summary",
+ NULL };
+ if (xb_builder_node_get_element (bn) != NULL &&
+ g_strv_contains (elements_to_tokenize, xb_builder_node_get_element (bn)))
+ xb_builder_node_tokenize_text (bn);
+ return TRUE;
+}
+#endif
+
+static gboolean
+gs_plugin_appstream_load_appstream_fn (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GFile) file = g_file_new_for_path (filename);
+ g_autoptr(XbBuilderNode) info = NULL;
+ g_autoptr(XbBuilderFixup) fixup1 = NULL;
+ g_autoptr(XbBuilderFixup) fixup2 = NULL;
+ g_autoptr(XbBuilderFixup) fixup3 = NULL;
+#if LIBXMLB_CHECK_VERSION(0,3,1)
+ g_autoptr(XbBuilderFixup) fixup4 = NULL;
+#endif
+ g_autoptr(XbBuilderFixup) fixup5 = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+
+ /* add support for DEP-11 files */
+ xb_builder_source_add_adapter (source,
+ "application/x-yaml",
+ gs_plugin_appstream_load_dep11_cb,
+ NULL, NULL);
+
+ /* add source */
+ if (!xb_builder_source_load_file (source, file,
+#if LIBXMLB_CHECK_VERSION(0, 2, 0)
+ XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY,
+#else
+ XB_BUILDER_SOURCE_FLAG_WATCH_FILE,
+#endif
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+
+ /* add metadata */
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "scope", "system", NULL);
+ xb_builder_node_insert_text (info, "filename", filename, NULL);
+ xb_builder_source_set_info (source, info);
+
+ /* add missing icons as required */
+ fixup1 = xb_builder_fixup_new ("AddIcons",
+ gs_plugin_appstream_add_icons_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup1, 2);
+ xb_builder_source_add_fixup (source, fixup1);
+
+ /* fix up any legacy installed files */
+ fixup2 = xb_builder_fixup_new ("AppStreamUpgrade2",
+ gs_plugin_appstream_upgrade_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup2, 3);
+ xb_builder_source_add_fixup (source, fixup2);
+
+ /* add the origin as a search keyword for small repos */
+ fixup3 = xb_builder_fixup_new ("AddOriginKeyword",
+ gs_plugin_appstream_add_origin_keyword_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup3, 1);
+ xb_builder_source_add_fixup (source, fixup3);
+
+#if LIBXMLB_CHECK_VERSION(0,3,1)
+ fixup4 = xb_builder_fixup_new ("TextTokenize",
+ gs_plugin_appstream_tokenize_cb,
+ NULL, NULL);
+ xb_builder_fixup_set_max_depth (fixup4, 2);
+ xb_builder_source_add_fixup (source, fixup4);
+#endif
+
+ /* prepend media_baseurl to remote relative URLs */
+ fixup5 = xb_builder_fixup_new ("MediaBaseUrl",
+ gs_plugin_appstream_media_baseurl_cb,
+ g_string_new (NULL),
+ gs_plugin_appstream_media_baseurl_free);
+ xb_builder_fixup_set_max_depth (fixup5, 3);
+ xb_builder_source_add_fixup (source, fixup5);
+
+ /* success */
+ xb_builder_import_source (builder, source);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_appstream_load_appstream (GsPluginAppstream *self,
+ XbBuilder *builder,
+ const gchar *path,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *fn;
+ g_autoptr(GDir) dir = NULL;
+ g_autoptr(GFile) parent = g_file_new_for_path (path);
+
+ /* parent path does not exist */
+ if (!g_file_query_exists (parent, cancellable)) {
+ g_debug ("appstream: Skipping appstream path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist");
+ return TRUE;
+ }
+ g_debug ("appstream: Loading appstream path '%s'", path);
+ dir = g_dir_open (path, 0, error);
+ if (dir == NULL)
+ return FALSE;
+ while ((fn = g_dir_read_name (dir)) != NULL) {
+#ifdef ENABLE_EXTERNAL_APPSTREAM
+ /* Ignore our own system-installed files when
+ external-appstream-system-wide is FALSE */
+ if (!g_settings_get_boolean (self->settings, "external-appstream-system-wide") &&
+ g_strcmp0 (path, gs_external_appstream_utils_get_system_dir ()) == 0 &&
+ g_str_has_prefix (fn, EXTERNAL_APPSTREAM_PREFIX))
+ continue;
+#endif
+ if (g_str_has_suffix (fn, ".xml") ||
+ g_str_has_suffix (fn, ".yml") ||
+ g_str_has_suffix (fn, ".yml.gz") ||
+ g_str_has_suffix (fn, ".xml.gz")) {
+ g_autofree gchar *filename = g_build_filename (path, fn, NULL);
+ g_autoptr(GError) error_local = NULL;
+ if (!gs_plugin_appstream_load_appstream_fn (self,
+ builder,
+ filename,
+ cancellable,
+ &error_local)) {
+ g_debug ("ignoring %s: %s", filename, error_local->message);
+ continue;
+ }
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static void
+gs_add_appstream_catalog_location (GPtrArray *locations, const gchar *root)
+{
+ g_autofree gchar *catalog_path = NULL;
+ g_autofree gchar *catalog_legacy_path = NULL;
+ gboolean ignore_legacy_path = FALSE;
+
+ catalog_path = g_build_filename (root, "swcatalog", NULL);
+ catalog_legacy_path = g_build_filename (root, "app-info", NULL);
+
+ /* ignore compatibility symlink if one exists, so we don't scan the same location twice */
+ if (g_file_test (catalog_legacy_path, G_FILE_TEST_IS_SYMLINK)) {
+ g_autofree gchar *link_target = g_file_read_link (catalog_legacy_path, NULL);
+ if (link_target != NULL) {
+ if (g_strcmp0 (link_target, catalog_path) == 0) {
+ ignore_legacy_path = TRUE;
+ g_debug ("Ignoring legacy AppStream catalog location '%s'.", catalog_legacy_path);
+ }
+ }
+ }
+
+ g_ptr_array_add (locations,
+ g_build_filename (catalog_path, "xml", NULL));
+ g_ptr_array_add (locations,
+ g_build_filename (catalog_path, "yaml", NULL));
+
+ if (!ignore_legacy_path) {
+ g_ptr_array_add (locations,
+ g_build_filename (catalog_legacy_path, "xml", NULL));
+ g_ptr_array_add (locations,
+ g_build_filename (catalog_legacy_path, "xmls", NULL));
+ g_ptr_array_add (locations,
+ g_build_filename (catalog_legacy_path, "yaml", NULL));
+ }
+}
+
+static void
+gs_add_appstream_metainfo_location (GPtrArray *locations, const gchar *root)
+{
+ g_ptr_array_add (locations,
+ g_build_filename (root, "metainfo", NULL));
+ g_ptr_array_add (locations,
+ g_build_filename (root, "appdata", NULL));
+}
+
+static gboolean
+gs_plugin_appstream_check_silo (GsPluginAppstream *self,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *test_xml;
+ g_autofree gchar *blobfn = NULL;
+ g_autoptr(XbBuilder) builder = NULL;
+ g_autoptr(XbNode) n = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GRWLockReaderLocker) reader_locker = NULL;
+ g_autoptr(GRWLockWriterLocker) writer_locker = NULL;
+ g_autoptr(GPtrArray) parent_appdata = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(GPtrArray) parent_appstream = g_ptr_array_new_with_free_func (g_free);
+ const gchar *const *locales = g_get_language_names ();
+ g_autoptr(GMainContext) old_thread_default = NULL;
+
+ reader_locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ /* everything is okay */
+ if (self->silo != NULL && xb_silo_is_valid (self->silo))
+ return TRUE;
+ g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free);
+
+ /* drat! silo needs regenerating */
+ writer_locker = g_rw_lock_writer_locker_new (&self->silo_lock);
+ g_clear_object (&self->silo);
+
+ /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */
+ old_thread_default = g_main_context_ref_thread_default ();
+ if (old_thread_default == g_main_context_default ())
+ g_clear_pointer (&old_thread_default, g_main_context_unref);
+ if (old_thread_default != NULL)
+ g_main_context_pop_thread_default (old_thread_default);
+ builder = xb_builder_new ();
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+ g_clear_pointer (&old_thread_default, g_main_context_unref);
+
+ /* verbose profiling */
+ if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
+ xb_builder_set_profile_flags (builder,
+ XB_SILO_PROFILE_FLAG_XPATH |
+ XB_SILO_PROFILE_FLAG_DEBUG);
+ }
+
+ /* add current locales */
+ for (guint i = 0; locales[i] != NULL; i++)
+ xb_builder_add_locale (builder, locales[i]);
+
+ /* only when in self test */
+ test_xml = g_getenv ("GS_SELF_TEST_APPSTREAM_XML");
+ if (test_xml != NULL) {
+ g_autoptr(XbBuilderFixup) fixup1 = NULL;
+ g_autoptr(XbBuilderFixup) fixup2 = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+ if (!xb_builder_source_load_xml (source, test_xml,
+ XB_BUILDER_SOURCE_FLAG_NONE,
+ error))
+ return FALSE;
+ fixup1 = xb_builder_fixup_new ("AddOriginKeywords",
+ gs_plugin_appstream_add_origin_keyword_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup1, 1);
+ xb_builder_source_add_fixup (source, fixup1);
+ fixup2 = xb_builder_fixup_new ("AddIcons",
+ gs_plugin_appstream_add_icons_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup2, 2);
+ xb_builder_source_add_fixup (source, fixup2);
+ xb_builder_import_source (builder, source);
+ } else {
+ g_autofree gchar *state_cache_dir = NULL;
+ g_autofree gchar *state_lib_dir = NULL;
+
+ /* add search paths */
+ gs_add_appstream_catalog_location (parent_appstream, DATADIR);
+ gs_add_appstream_metainfo_location (parent_appdata, DATADIR);
+
+ state_cache_dir = g_build_filename (LOCALSTATEDIR, "cache", NULL);
+ gs_add_appstream_catalog_location (parent_appstream, state_cache_dir);
+ state_lib_dir = g_build_filename (LOCALSTATEDIR, "lib", NULL);
+ gs_add_appstream_catalog_location (parent_appstream, state_lib_dir);
+
+#ifdef ENABLE_EXTERNAL_APPSTREAM
+ /* check for the corresponding setting */
+ if (!g_settings_get_boolean (self->settings, "external-appstream-system-wide")) {
+ g_autofree gchar *user_catalog_path = NULL;
+ g_autofree gchar *user_catalog_old_path = NULL;
+
+ /* migrate data paths */
+ user_catalog_path = g_build_filename (g_get_user_data_dir (), "swcatalog", NULL);
+ user_catalog_old_path = g_build_filename (g_get_user_data_dir (), "app-info", NULL);
+ if (g_file_test (user_catalog_old_path, G_FILE_TEST_IS_DIR) &&
+ !g_file_test (user_catalog_path, G_FILE_TEST_IS_DIR)) {
+ g_debug ("Migrating external AppStream user location.");
+ if (g_rename (user_catalog_old_path, user_catalog_path) == 0) {
+ g_autofree gchar *user_catalog_xml_path = NULL;
+ g_autofree gchar *user_catalog_xml_old_path = NULL;
+
+ user_catalog_xml_path = g_build_filename (user_catalog_path, "xml", NULL);
+ user_catalog_xml_old_path = g_build_filename (user_catalog_path, "xmls", NULL);
+ if (g_file_test (user_catalog_xml_old_path, G_FILE_TEST_IS_DIR)) {
+ if (g_rename (user_catalog_xml_old_path, user_catalog_xml_path) != 0)
+ g_warning ("Unable to migrate external XML data location from '%s' to '%s': %s",
+ user_catalog_xml_old_path, user_catalog_xml_path, g_strerror (errno));
+ }
+ } else {
+ g_warning ("Unable to migrate external data location from '%s' to '%s': %s",
+ user_catalog_old_path, user_catalog_path, g_strerror (errno));
+ }
+
+ }
+
+ /* add modern locations only */
+ g_ptr_array_add (parent_appstream,
+ g_build_filename (user_catalog_path, "xml", NULL));
+ g_ptr_array_add (parent_appstream,
+ g_build_filename (user_catalog_path, "yaml", NULL));
+ }
+#endif
+
+ /* Add the normal system directories if the installation prefix
+ * is different from normal — typically this happens when doing
+ * development builds. It’s useful to still list the system apps
+ * during development. */
+ if (g_strcmp0 (DATADIR, "/usr/share") != 0) {
+ gs_add_appstream_catalog_location (parent_appstream, "/usr/share");
+ gs_add_appstream_metainfo_location (parent_appdata, "/usr/share");
+ }
+ if (g_strcmp0 (LOCALSTATEDIR, "/var") != 0) {
+ gs_add_appstream_catalog_location (parent_appstream, "/var/cache");
+ gs_add_appstream_catalog_location (parent_appstream, "/var/lib");
+ }
+
+ /* import all files */
+ for (guint i = 0; i < parent_appstream->len; i++) {
+ const gchar *fn = g_ptr_array_index (parent_appstream, i);
+ if (!gs_plugin_appstream_load_appstream (self, builder, fn,
+ cancellable, error))
+ return FALSE;
+ }
+ for (guint i = 0; i < parent_appdata->len; i++) {
+ const gchar *fn = g_ptr_array_index (parent_appdata, i);
+ if (!gs_plugin_appstream_load_appdata (self, builder, fn,
+ cancellable, error))
+ return FALSE;
+ }
+ if (!gs_plugin_appstream_load_desktop (self, builder,
+ DATADIR "/applications",
+ cancellable, error)) {
+ return FALSE;
+ }
+ if (g_strcmp0 (DATADIR, "/usr/share") != 0 &&
+ !gs_plugin_appstream_load_desktop (self, builder,
+ "/usr/share/applications",
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+
+ /* regenerate with each minor release */
+ xb_builder_append_guid (builder, PACKAGE_VERSION);
+
+ /* create per-user cache */
+ blobfn = gs_utils_get_cache_filename ("appstream", "components.xmlb",
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
+ error);
+ if (blobfn == NULL)
+ return FALSE;
+ file = g_file_new_for_path (blobfn);
+ g_debug ("ensuring %s", blobfn);
+
+ /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */
+ old_thread_default = g_main_context_ref_thread_default ();
+ if (old_thread_default == g_main_context_default ())
+ g_clear_pointer (&old_thread_default, g_main_context_unref);
+ if (old_thread_default != NULL)
+ g_main_context_pop_thread_default (old_thread_default);
+
+ self->silo = xb_builder_ensure (builder, file,
+ XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
+ XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+ NULL, error);
+ if (self->silo == NULL) {
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+ return FALSE;
+ }
+
+ /* watch all directories too */
+ for (guint i = 0; i < parent_appstream->len; i++) {
+ const gchar *fn = g_ptr_array_index (parent_appstream, i);
+ g_autoptr(GFile) file_tmp = g_file_new_for_path (fn);
+ if (!xb_silo_watch_file (self->silo, file_tmp, cancellable, error)) {
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+ return FALSE;
+ }
+ }
+ for (guint i = 0; i < parent_appdata->len; i++) {
+ const gchar *fn = g_ptr_array_index (parent_appdata, i);
+ g_autoptr(GFile) file_tmp = g_file_new_for_path (fn);
+ if (!xb_silo_watch_file (self->silo, file_tmp, cancellable, error)) {
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+ return FALSE;
+ }
+ }
+
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+
+ /* test we found something */
+ n = xb_silo_query_first (self->silo, "components/component", NULL);
+ if (n == NULL) {
+ g_warning ("No AppStream data, try 'make install-sample-data' in data/");
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "No AppStream data found");
+ return FALSE;
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static gint
+get_priority_for_interactivity (gboolean interactive)
+{
+ return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW;
+}
+
+static void setup_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_appstream_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_setup_async);
+
+ /* Start up a worker thread to process all the plugin’s function calls. */
+ self->worker = gs_worker_thread_new ("gs-plugin-appstream");
+
+ /* Queue a job to check the silo, which will cause it to be loaded. */
+ gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT,
+ setup_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+setup_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error))
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ else
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_appstream_shutdown_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_shutdown_async);
+
+ /* Stop the worker thread. */
+ gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
+}
+
+static void
+shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = G_TASK (user_data);
+ GsPluginAppstream *self = g_task_get_source_object (task);
+ g_autoptr(GsWorkerThread) worker = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ worker = g_steal_pointer (&self->worker);
+
+ if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_shutdown_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+gs_plugin_url_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ /* check silo is valid */
+ if (!gs_plugin_appstream_check_silo (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ return gs_appstream_url_to_app (plugin, self->silo, list, url, cancellable, error);
+}
+
+static void
+gs_plugin_appstream_set_compulsory_quirk (GsApp *app, XbNode *component)
+{
+ g_autoptr(GPtrArray) array = NULL;
+ const gchar *current_desktop;
+
+ /*
+ * Set the core applications for the current desktop that cannot be
+ * removed.
+ *
+ * If XDG_CURRENT_DESKTOP contains ":", indicating that it is made up
+ * of multiple components per the Desktop Entry Specification, an app
+ * is compulsory if any of the components in XDG_CURRENT_DESKTOP match
+ * any value in <compulsory_for_desktops />. In that way,
+ * "GNOME-Classic:GNOME" shares compulsory apps with GNOME.
+ *
+ * As a special case, if the <compulsory_for_desktop /> value contains
+ * a ":", we match the entire XDG_CURRENT_DESKTOP. This lets people set
+ * compulsory apps for such compound desktops if they want.
+ *
+ */
+ array = xb_node_query (component, "compulsory_for_desktop", 0, NULL);
+ if (array == NULL)
+ return;
+ current_desktop = g_getenv ("XDG_CURRENT_DESKTOP");
+ if (current_desktop != NULL) {
+ g_auto(GStrv) xdg_current_desktops = g_strsplit (current_desktop, ":", 0);
+ for (guint i = 0; i < array->len; i++) {
+ XbNode *n = g_ptr_array_index (array, i);
+ const gchar *tmp = xb_node_get_text (n);
+ /* if the value has a :, check the whole string */
+ if (g_strstr_len (tmp, -1, ":")) {
+ if (g_strcmp0 (current_desktop, tmp) == 0) {
+ gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
+ break;
+ }
+ /* otherwise check if any element matches this one */
+ } else if (g_strv_contains ((const gchar * const *) xdg_current_desktops, tmp)) {
+ gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
+ break;
+ }
+ }
+ }
+}
+
+static gboolean
+gs_plugin_appstream_refine_state (GsPluginAppstream *self,
+ GsApp *app,
+ GError **error)
+{
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(XbNode) component = NULL;
+
+ /* Ignore apps with no ID */
+ if (gs_app_get_id (app) == NULL)
+ return TRUE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ xpath = g_strdup_printf ("component/id[text()='%s']", gs_app_get_id (app));
+ component = xb_silo_query_first (self->silo, xpath, &error_local);
+ if (component == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_refine_from_id (GsPluginAppstream *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean *found,
+ GError **error)
+{
+ const gchar *id, *origin;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GString) xpath = g_string_new (NULL);
+ g_autoptr(GPtrArray) components = NULL;
+
+ /* not enough info to find */
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ return TRUE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ origin = gs_app_get_origin_appstream (app);
+
+ /* look in AppStream then fall back to AppData */
+ if (origin && *origin) {
+ xb_string_append_union (xpath, "components[@origin='%s']/component/id[text()='%s']/../pkgname/..", origin, id);
+ xb_string_append_union (xpath, "components[@origin='%s']/component[@type='web-application']/id[text()='%s']/..", origin, id);
+ } else {
+ xb_string_append_union (xpath, "components/component/id[text()='%s']/../pkgname/..", id);
+ xb_string_append_union (xpath, "components/component[@type='web-application']/id[text()='%s']/..", id);
+ }
+ xb_string_append_union (xpath, "component/id[text()='%s']/..", id);
+ components = xb_silo_query (self->silo, xpath->str, 0, &error_local);
+ if (components == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ if (!gs_appstream_refine_app (GS_PLUGIN (self), app, self->silo,
+ component, flags, error))
+ return FALSE;
+ gs_plugin_appstream_set_compulsory_quirk (app, component);
+ }
+
+ /* if an installed desktop or appdata file exists set to installed */
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
+ if (!gs_plugin_appstream_refine_state (self, app, error))
+ return FALSE;
+ }
+
+ /* success */
+ *found = TRUE;
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_refine_from_pkgname (GsPluginAppstream *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GError **error)
+{
+ GPtrArray *sources = gs_app_get_sources (app);
+ g_autoptr(GError) error_local = NULL;
+
+ /* not enough info to find */
+ if (sources->len == 0)
+ return TRUE;
+
+ /* find all apps when matching any prefixes */
+ for (guint j = 0; j < sources->len; j++) {
+ const gchar *pkgname = g_ptr_array_index (sources, j);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GString) xpath = g_string_new (NULL);
+ g_autoptr(XbNode) component = NULL;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ /* prefer actual apps and then fallback to anything else */
+ xb_string_append_union (xpath, "components/component[@type='desktop-application']/pkgname[text()='%s']/..", pkgname);
+ xb_string_append_union (xpath, "components/component[@type='console-application']/pkgname[text()='%s']/..", pkgname);
+ xb_string_append_union (xpath, "components/component[@type='web-application']/pkgname[text()='%s']/..", pkgname);
+ xb_string_append_union (xpath, "components/component/pkgname[text()='%s']/..", pkgname);
+ component = xb_silo_query_first (self->silo, xpath->str, &error_local);
+ if (component == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ continue;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ if (!gs_appstream_refine_app (GS_PLUGIN (self), app, self->silo, component, flags, error))
+ return FALSE;
+ gs_plugin_appstream_set_compulsory_quirk (app, component);
+ }
+
+ /* if an installed desktop or appdata file exists set to installed */
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
+ if (!gs_plugin_appstream_refine_state (self, app, error))
+ return FALSE;
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static void refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_appstream_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_refine_async);
+
+ /* Queue a job for the refine. */
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ refine_thread_cb, g_steal_pointer (&task));
+}
+
+static gboolean refine_wildcard (GsPluginAppstream *self,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+
+/* Run in @worker. */
+static void
+refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
+ GsPluginRefineData *data = task_data;
+ GsAppList *list = data->list;
+ GsPluginRefineFlags flags = data->flags;
+ gboolean found = FALSE;
+ g_autoptr(GsAppList) app_list = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ /* check silo is valid */
+ if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ /* not us */
+ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE &&
+ gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_UNKNOWN)
+ continue;
+
+ /* find by ID then fall back to package name */
+ if (!gs_plugin_refine_from_id (self, app, flags, &found, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ if (!found) {
+ if (!gs_plugin_refine_from_pkgname (self, app, flags, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+ }
+
+ /* Refine wildcards.
+ *
+ * Use a copy of the list for the loop because a function called
+ * on the plugin may affect the list which can lead to problems
+ * (e.g. inserting an app in the list on every call results in
+ * an infinite loop) */
+ app_list = gs_app_list_copy (list);
+
+ for (guint j = 0; j < gs_app_list_length (app_list); j++) {
+ GsApp *app = gs_app_list_index (app_list, j);
+
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) &&
+ !refine_wildcard (self, app, list, flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ /* success */
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/* Run in @worker. Silo must be valid */
+static gboolean
+refine_wildcard (GsPluginAppstream *self,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *id;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GPtrArray) components = NULL;
+
+ /* not enough info to find */
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ return TRUE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ /* find all app with package names when matching any prefixes */
+ xpath = g_strdup_printf ("components/component/id[text()='%s']/../pkgname/..", id);
+ components = xb_silo_query (self->silo, xpath, 0, &error_local);
+ if (components == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) new = NULL;
+
+ /* new app */
+ new = gs_appstream_create_app (GS_PLUGIN (self), self->silo, component, error);
+ if (new == NULL)
+ return FALSE;
+ gs_app_set_scope (new, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_subsume_metadata (new, app);
+ if (!gs_appstream_refine_app (GS_PLUGIN (self), new, self->silo, component,
+ refine_flags, error))
+ return FALSE;
+ gs_plugin_appstream_set_compulsory_quirk (new, component);
+
+ /* if an installed desktop or appdata file exists set to installed */
+ if (gs_app_get_state (new) == GS_APP_STATE_UNKNOWN) {
+ if (!gs_plugin_appstream_refine_state (self, new, error))
+ return FALSE;
+ }
+
+ gs_app_list_add (list, new);
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static void refine_categories_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_appstream_refine_categories_async (GsPlugin *plugin,
+ GPtrArray *list,
+ GsPluginRefineCategoriesFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_refine_categories_data_new_task (plugin, list, flags,
+ cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_refine_categories_async);
+
+ /* All we actually do is add the sizes of each category. If that’s
+ * not been requested, avoid queueing a worker job. */
+ if (!(flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* Queue a job to get the apps. */
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ refine_categories_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+refine_categories_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ GsPluginRefineCategoriesData *data = task_data;
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ /* check silo is valid */
+ if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ if (!gs_appstream_refine_category_sizes (self->silo, data->list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_refine_categories_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void list_apps_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_appstream_list_apps_async (GsPlugin *plugin,
+ GsAppQuery *query,
+ GsPluginListAppsFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_list_apps_data_new_task (plugin, query, flags,
+ cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_list_apps_async);
+
+ /* Queue a job to get the apps. */
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ list_apps_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+list_apps_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ GsPluginListAppsData *data = task_data;
+ GDateTime *released_since = NULL;
+ GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET;
+ GsAppQueryTristate is_featured = GS_APP_QUERY_TRISTATE_UNSET;
+ GsCategory *category = NULL;
+ GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET;
+ guint64 age_secs = 0;
+ const gchar * const *deployment_featured = NULL;
+ const gchar * const *developers = NULL;
+ const gchar * const *keywords = NULL;
+ GsApp *alternate_of = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ if (data->query != NULL) {
+ released_since = gs_app_query_get_released_since (data->query);
+ is_curated = gs_app_query_get_is_curated (data->query);
+ is_featured = gs_app_query_get_is_featured (data->query);
+ category = gs_app_query_get_category (data->query);
+ is_installed = gs_app_query_get_is_installed (data->query);
+ deployment_featured = gs_app_query_get_deployment_featured (data->query);
+ developers = gs_app_query_get_developers (data->query);
+ keywords = gs_app_query_get_keywords (data->query);
+ alternate_of = gs_app_query_get_alternate_of (data->query);
+ }
+ if (released_since != NULL) {
+ g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
+ age_secs = g_date_time_difference (now, released_since) / G_TIME_SPAN_SECOND;
+ }
+
+ /* Currently only support a subset of query properties, and only one set at once.
+ * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */
+ if ((released_since == NULL &&
+ is_curated == GS_APP_QUERY_TRISTATE_UNSET &&
+ is_featured == GS_APP_QUERY_TRISTATE_UNSET &&
+ category == NULL &&
+ is_installed == GS_APP_QUERY_TRISTATE_UNSET &&
+ deployment_featured == NULL &&
+ developers == NULL &&
+ keywords == NULL &&
+ alternate_of == NULL) ||
+ is_curated == GS_APP_QUERY_TRISTATE_FALSE ||
+ is_featured == GS_APP_QUERY_TRISTATE_FALSE ||
+ is_installed == GS_APP_QUERY_TRISTATE_FALSE ||
+ gs_app_query_get_n_properties_set (data->query) != 1) {
+ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "Unsupported query");
+ return;
+ }
+
+ /* check silo is valid */
+ if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ if (released_since != NULL &&
+ !gs_appstream_add_recent (GS_PLUGIN (self), self->silo, list, age_secs,
+ cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_curated != GS_APP_QUERY_TRISTATE_UNSET &&
+ !gs_appstream_add_popular (self->silo, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_featured != GS_APP_QUERY_TRISTATE_UNSET &&
+ !gs_appstream_add_featured (self->silo, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (category != NULL &&
+ !gs_appstream_add_category_apps (GS_PLUGIN (self), self->silo, category, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_installed == GS_APP_QUERY_TRISTATE_TRUE &&
+ !gs_appstream_add_installed (GS_PLUGIN (self), self->silo, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (deployment_featured != NULL &&
+ !gs_appstream_add_deployment_featured (self->silo, deployment_featured, list,
+ cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (developers != NULL &&
+ !gs_appstream_search_developer_apps (GS_PLUGIN (self), self->silo, developers, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (keywords != NULL &&
+ !gs_appstream_search (GS_PLUGIN (self), self->silo, keywords, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (alternate_of != NULL &&
+ !gs_appstream_add_alternates (self->silo, alternate_of, list, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
+}
+
+static GsAppList *
+gs_plugin_appstream_list_apps_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void refresh_metadata_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_appstream_refresh_metadata_async (GsPlugin *plugin,
+ guint64 cache_age_secs,
+ GsPluginRefreshMetadataFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE);
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_appstream_refresh_metadata_async);
+
+ /* Queue a job to check the silo, which will cause it to be refreshed if needed. */
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ refresh_metadata_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+refresh_metadata_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ /* Checking the silo will refresh it if needed. */
+ if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error))
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ else
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_appstream_refresh_metadata_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_appstream_class_init (GsPluginAppstreamClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_appstream_dispose;
+
+ plugin_class->setup_async = gs_plugin_appstream_setup_async;
+ plugin_class->setup_finish = gs_plugin_appstream_setup_finish;
+ plugin_class->shutdown_async = gs_plugin_appstream_shutdown_async;
+ plugin_class->shutdown_finish = gs_plugin_appstream_shutdown_finish;
+ plugin_class->refine_async = gs_plugin_appstream_refine_async;
+ plugin_class->refine_finish = gs_plugin_appstream_refine_finish;
+ plugin_class->list_apps_async = gs_plugin_appstream_list_apps_async;
+ plugin_class->list_apps_finish = gs_plugin_appstream_list_apps_finish;
+ plugin_class->refresh_metadata_async = gs_plugin_appstream_refresh_metadata_async;
+ plugin_class->refresh_metadata_finish = gs_plugin_appstream_refresh_metadata_finish;
+ plugin_class->refine_categories_async = gs_plugin_appstream_refine_categories_async;
+ plugin_class->refine_categories_finish = gs_plugin_appstream_refine_categories_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_APPSTREAM;
+}
diff --git a/plugins/core/gs-plugin-appstream.h b/plugins/core/gs-plugin-appstream.h
new file mode 100644
index 0000000..3063ada
--- /dev/null
+++ b/plugins/core/gs-plugin-appstream.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_APPSTREAM (gs_plugin_appstream_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginAppstream, gs_plugin_appstream, GS, PLUGIN_APPSTREAM, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-generic-updates.c b/plugins/core/gs-plugin-generic-updates.c
new file mode 100644
index 0000000..06a16f1
--- /dev/null
+++ b/plugins/core/gs-plugin-generic-updates.c
@@ -0,0 +1,148 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <gnome-software.h>
+
+#include "gs-plugin-generic-updates.h"
+
+struct _GsPluginGenericUpdates
+{
+ GsPlugin parent;
+};
+
+G_DEFINE_TYPE (GsPluginGenericUpdates, gs_plugin_generic_updates, GS_TYPE_PLUGIN)
+
+static void
+gs_plugin_generic_updates_init (GsPluginGenericUpdates *self)
+{
+ GsPlugin *plugin = GS_PLUGIN (self);
+
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit");
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree");
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons");
+}
+
+static gboolean
+gs_plugin_generic_updates_merge_os_update (GsApp *app)
+{
+ /* this is only for grouping system-installed packages */
+ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE ||
+ gs_app_get_scope (app) != AS_COMPONENT_SCOPE_SYSTEM)
+ return FALSE;
+
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC)
+ return TRUE;
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
+ return TRUE;
+
+ return FALSE;
+}
+
+static GsApp *
+gs_plugin_generic_updates_get_os_update (GsPlugin *plugin)
+{
+ GsApp *app;
+ const gchar *id = "org.gnome.Software.OsUpdate";
+ g_autoptr(GIcon) ic = NULL;
+
+ /* create new */
+ app = gs_app_new (id);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_PROXY);
+ gs_app_set_management_plugin (app, plugin);
+ gs_app_set_special_kind (app, GS_APP_SPECIAL_KIND_OS_UPDATE);
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+ gs_app_set_name (app,
+ GS_APP_QUALITY_NORMAL,
+ /* TRANSLATORS: this is a group of updates that are not
+ * packages and are not shown in the main list */
+ _("System Updates"));
+ gs_app_set_summary (app,
+ GS_APP_QUALITY_NORMAL,
+ /* TRANSLATORS: this is a longer description of the
+ * "System Updates" string */
+ _("General system updates, such as security or bug fixes, and performance improvements."));
+ gs_app_set_description (app,
+ GS_APP_QUALITY_NORMAL,
+ gs_app_get_summary (app));
+ ic = g_themed_icon_new ("system-component-os-updates");
+ gs_app_add_icon (app, ic);
+ return app;
+}
+
+static void
+gs_plugin_generic_updates_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) os_updates = gs_app_list_new ();
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_generic_updates_refine_async);
+
+ /* not from get_updates() */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) == 0) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* do we have any packages left that are not apps? */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ if (gs_plugin_generic_updates_merge_os_update (app_tmp))
+ gs_app_list_add (os_updates, app_tmp);
+ }
+ if (gs_app_list_length (os_updates) == 0) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* create new meta object */
+ app = gs_plugin_generic_updates_get_os_update (plugin);
+ for (guint i = 0; i < gs_app_list_length (os_updates); i++) {
+ GsApp *app_tmp = gs_app_list_index (os_updates, i);
+ gs_app_add_related (app, app_tmp);
+ gs_app_list_remove (list, app_tmp);
+ }
+ gs_app_list_add (list, app);
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_generic_updates_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_generic_updates_class_init (GsPluginGenericUpdatesClass *klass)
+{
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ plugin_class->refine_async = gs_plugin_generic_updates_refine_async;
+ plugin_class->refine_finish = gs_plugin_generic_updates_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_GENERIC_UPDATES;
+}
diff --git a/plugins/core/gs-plugin-generic-updates.h b/plugins/core/gs-plugin-generic-updates.h
new file mode 100644
index 0000000..1b2448c
--- /dev/null
+++ b/plugins/core/gs-plugin-generic-updates.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_GENERIC_UPDATES (gs_plugin_generic_updates_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginGenericUpdates, gs_plugin_generic_updates, GS, PLUGIN_GENERIC_UPDATES, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-hardcoded-blocklist.c b/plugins/core/gs-plugin-hardcoded-blocklist.c
new file mode 100644
index 0000000..f943496
--- /dev/null
+++ b/plugins/core/gs-plugin-hardcoded-blocklist.c
@@ -0,0 +1,121 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <fnmatch.h>
+#include <gnome-software.h>
+
+#include "gs-plugin-hardcoded-blocklist.h"
+
+/*
+ * SECTION:
+ * Blocklists some applications based on a hardcoded list.
+ *
+ * This plugin executes entirely in the main thread.
+ */
+
+struct _GsPluginHardcodedBlocklist
+{
+ GsPlugin parent;
+};
+
+G_DEFINE_TYPE (GsPluginHardcodedBlocklist, gs_plugin_hardcoded_blocklist, GS_TYPE_PLUGIN)
+
+static void
+gs_plugin_hardcoded_blocklist_init (GsPluginHardcodedBlocklist *self)
+{
+ /* need ID */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+}
+
+static gboolean
+refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint i;
+ const gchar *app_globs[] = {
+ "freeciv-server.desktop",
+ "links.desktop",
+ "nm-connection-editor.desktop",
+ "plank.desktop",
+ "*release-notes*.desktop",
+ "*Release_Notes*.desktop",
+ "Rodent-*.desktop",
+ "rygel-preferences.desktop",
+ "system-config-keyboard.desktop",
+ "tracker-preferences.desktop",
+ "Uninstall*.desktop",
+ "wine-*.desktop",
+ NULL };
+
+ /* not set yet */
+ if (gs_app_get_id (app) == NULL)
+ return TRUE;
+
+ /* search */
+ for (i = 0; app_globs[i] != NULL; i++) {
+ if (fnmatch (app_globs[i], gs_app_get_id (app), 0) == 0) {
+ gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ break;
+ }
+ }
+
+ return TRUE;
+}
+
+static void
+gs_plugin_hardcoded_blocklist_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_hardcoded_blocklist_refine_async);
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (!refine_app (plugin, app, flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_hardcoded_blocklist_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_hardcoded_blocklist_class_init (GsPluginHardcodedBlocklistClass *klass)
+{
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ plugin_class->refine_async = gs_plugin_hardcoded_blocklist_refine_async;
+ plugin_class->refine_finish = gs_plugin_hardcoded_blocklist_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_HARDCODED_BLOCKLIST;
+}
diff --git a/plugins/core/gs-plugin-hardcoded-blocklist.h b/plugins/core/gs-plugin-hardcoded-blocklist.h
new file mode 100644
index 0000000..fceef62
--- /dev/null
+++ b/plugins/core/gs-plugin-hardcoded-blocklist.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_HARDCODED_BLOCKLIST (gs_plugin_hardcoded_blocklist_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginHardcodedBlocklist, gs_plugin_hardcoded_blocklist, GS, PLUGIN_HARDCODED_BLOCKLIST, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-icons.c b/plugins/core/gs-plugin-icons.c
new file mode 100644
index 0000000..0dbb8ba
--- /dev/null
+++ b/plugins/core/gs-plugin-icons.c
@@ -0,0 +1,249 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2014 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <libsoup/soup.h>
+#include <string.h>
+
+#include <gnome-software.h>
+
+#include "gs-plugin-icons.h"
+
+/*
+ * SECTION:
+ * Loads remote icons and converts them into local cached ones.
+ *
+ * It is provided so that each plugin handling icons does not
+ * have to handle the download and caching functionality.
+ *
+ * FIXME: This plugin will eventually go away. Currently it only exists as the
+ * plugin threading code is a convenient way of ensuring that loading the remote
+ * icons happens in a worker thread.
+ */
+
+struct _GsPluginIcons
+{
+ GsPlugin parent;
+
+ SoupSession *soup_session; /* (owned) */
+ GsWorkerThread *worker; /* (owned) */
+};
+
+G_DEFINE_TYPE (GsPluginIcons, gs_plugin_icons, GS_TYPE_PLUGIN)
+
+#define assert_in_worker(self) \
+ g_assert (gs_worker_thread_is_in_worker_context (self->worker))
+
+static void
+gs_plugin_icons_init (GsPluginIcons *self)
+{
+ /* needs remote icons downloaded */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "epiphany");
+}
+
+static void
+gs_plugin_icons_dispose (GObject *object)
+{
+ GsPluginIcons *self = GS_PLUGIN_ICONS (object);
+
+ g_clear_object (&self->soup_session);
+ g_clear_object (&self->worker);
+
+ G_OBJECT_CLASS (gs_plugin_icons_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_icons_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginIcons *self = GS_PLUGIN_ICONS (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_icons_setup_async);
+
+ self->soup_session = gs_build_soup_session ();
+
+ /* Start up a worker thread to process all the plugin’s function calls. */
+ self->worker = gs_worker_thread_new ("gs-plugin-icons");
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_icons_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_icons_shutdown_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginIcons *self = GS_PLUGIN_ICONS (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_icons_shutdown_async);
+
+ /* Stop the worker thread. */
+ gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
+}
+
+static void
+shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = G_TASK (user_data);
+ GsPluginIcons *self = g_task_get_source_object (task);
+ g_autoptr(GsWorkerThread) worker = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ g_clear_object (&self->soup_session);
+ worker = g_steal_pointer (&self->worker);
+
+ if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_icons_shutdown_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static gboolean
+refine_app (GsPluginIcons *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint maximum_icon_size;
+
+ assert_in_worker (self);
+
+ /* not required */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0)
+ return TRUE;
+
+ /* Currently a 160px icon is needed for #GsFeatureTile, at most. */
+ maximum_icon_size = 160 * gs_plugin_get_scale (GS_PLUGIN (self));
+
+ gs_app_ensure_icons_downloaded (app, self->soup_session, maximum_icon_size, cancellable);
+
+ return TRUE;
+}
+
+static void refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_icons_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginIcons *self = GS_PLUGIN_ICONS (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_icons_refine_async);
+
+ /* nothing to do here */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* Queue a job for the refine. */
+ gs_worker_thread_queue (self->worker, interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW,
+ refine_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginIcons *self = GS_PLUGIN_ICONS (source_object);
+ GsPluginRefineData *data = task_data;
+ GsAppList *list = data->list;
+ GsPluginRefineFlags flags = data->flags;
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ if (!refine_app (self, app, flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_icons_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_icons_class_init (GsPluginIconsClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_icons_dispose;
+
+ plugin_class->setup_async = gs_plugin_icons_setup_async;
+ plugin_class->setup_finish = gs_plugin_icons_setup_finish;
+ plugin_class->shutdown_async = gs_plugin_icons_shutdown_async;
+ plugin_class->shutdown_finish = gs_plugin_icons_shutdown_finish;
+ plugin_class->refine_async = gs_plugin_icons_refine_async;
+ plugin_class->refine_finish = gs_plugin_icons_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_ICONS;
+}
diff --git a/plugins/core/gs-plugin-icons.h b/plugins/core/gs-plugin-icons.h
new file mode 100644
index 0000000..e092d0d
--- /dev/null
+++ b/plugins/core/gs-plugin-icons.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_ICONS (gs_plugin_icons_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginIcons, gs_plugin_icons, GS, PLUGIN_ICONS, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-os-release.c b/plugins/core/gs-plugin-os-release.c
new file mode 100644
index 0000000..685af27
--- /dev/null
+++ b/plugins/core/gs-plugin-os-release.c
@@ -0,0 +1,180 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <gnome-software.h>
+
+#include "gs-plugin-os-release.h"
+
+struct _GsPluginOsRelease
+{
+ GsPlugin parent;
+
+ GsApp *app_system;
+};
+
+G_DEFINE_TYPE (GsPluginOsRelease, gs_plugin_os_release, GS_TYPE_PLUGIN)
+
+static void
+gs_plugin_os_release_dispose (GObject *object)
+{
+ GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (object);
+
+ g_clear_object (&self->app_system);
+
+ G_OBJECT_CLASS (gs_plugin_os_release_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_os_release_init (GsPluginOsRelease *self)
+{
+ self->app_system = gs_app_new ("system");
+ gs_app_set_kind (self->app_system, AS_COMPONENT_KIND_OPERATING_SYSTEM);
+ gs_app_set_state (self->app_system, GS_APP_STATE_INSTALLED);
+}
+
+static void
+gs_plugin_os_release_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (plugin);
+ g_autoptr(GTask) task = NULL;
+ const gchar *cpe_name;
+ const gchar *home_url;
+ const gchar *name;
+ const gchar *version;
+ const gchar *os_id;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_os_release_setup_async);
+
+ /* parse os-release, wherever it may be */
+ os_release = gs_os_release_new (&local_error);
+ if (os_release == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ cpe_name = gs_os_release_get_cpe_name (os_release);
+ if (cpe_name != NULL)
+ gs_app_set_metadata (self->app_system, "GnomeSoftware::CpeName", cpe_name);
+ name = gs_os_release_get_name (os_release);
+ if (name != NULL)
+ gs_app_set_name (self->app_system, GS_APP_QUALITY_LOWEST, name);
+ version = gs_os_release_get_version_id (os_release);
+ if (version != NULL)
+ gs_app_set_version (self->app_system, version);
+
+ os_id = gs_os_release_get_id (os_release);
+
+ /* use libsoup to convert a URL */
+ home_url = gs_os_release_get_home_url (os_release);
+ if (home_url != NULL) {
+ g_autoptr(GUri) uri = NULL;
+
+ /* homepage */
+ gs_app_set_url (self->app_system, AS_URL_KIND_HOMEPAGE, home_url);
+
+ /* Build ID from the reverse-DNS URL and the ID and version. */
+ uri = g_uri_parse (home_url, SOUP_HTTP_URI_FLAGS, NULL);
+ if (uri != NULL) {
+ g_auto(GStrv) split = NULL;
+ const gchar *home_host = g_uri_get_host (uri);
+ split = g_strsplit_set (home_host, ".", -1);
+ if (g_strv_length (split) >= 2) {
+ g_autofree gchar *id = NULL;
+ id = g_strdup_printf ("%s.%s.%s-%s",
+ split[1],
+ split[0],
+ (os_id != NULL) ? os_id : "unnamed",
+ (version != NULL) ? version : "unversioned");
+ gs_app_set_id (self->app_system, id);
+ }
+ }
+ }
+
+ /* success */
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_os_release_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_os_release_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_os_release_refine_async);
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ /* match meta-id */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) &&
+ g_strcmp0 (gs_app_get_id (app), "system") == 0) {
+ /* copy over interesting metadata */
+ if (gs_app_get_install_date (app) != 0 &&
+ gs_app_get_install_date (self->app_system) == 0) {
+ gs_app_set_install_date (self->app_system,
+ gs_app_get_install_date (app));
+ }
+
+ gs_app_list_add (list, self->app_system);
+ break;
+ }
+ }
+
+ /* success */
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_os_release_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_os_release_class_init (GsPluginOsReleaseClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_os_release_dispose;
+
+ plugin_class->setup_async = gs_plugin_os_release_setup_async;
+ plugin_class->setup_finish = gs_plugin_os_release_setup_finish;
+ plugin_class->refine_async = gs_plugin_os_release_refine_async;
+ plugin_class->refine_finish = gs_plugin_os_release_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_OS_RELEASE;
+}
diff --git a/plugins/core/gs-plugin-os-release.h b/plugins/core/gs-plugin-os-release.h
new file mode 100644
index 0000000..9ab9d0d
--- /dev/null
+++ b/plugins/core/gs-plugin-os-release.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_OS_RELEASE (gs_plugin_os_release_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginOsRelease, gs_plugin_os_release, GS, PLUGIN_OS_RELEASE, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-provenance-license.c b/plugins/core/gs-plugin-provenance-license.c
new file mode 100644
index 0000000..8ddd071
--- /dev/null
+++ b/plugins/core/gs-plugin-provenance-license.c
@@ -0,0 +1,199 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2016 Matthias Klumpp <mak@debian.org>
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <gnome-software.h>
+
+#include "gs-plugin-provenance-license.h"
+
+/*
+ * SECTION:
+ * Marks the application as Free Software if it comes from an origin
+ * that is recognized as being DFSGish-free.
+ *
+ * This plugin executes entirely in the main thread.
+ */
+
+struct _GsPluginProvenanceLicense {
+ GsPlugin parent;
+
+ GSettings *settings;
+ gchar **sources;
+ gchar *license_id;
+};
+
+G_DEFINE_TYPE (GsPluginProvenanceLicense, gs_plugin_provenance_license, GS_TYPE_PLUGIN)
+
+static gchar **
+gs_plugin_provenance_license_get_sources (GsPluginProvenanceLicense *self)
+{
+ const gchar *tmp;
+
+ tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_SOURCES");
+ if (tmp != NULL) {
+ g_debug ("using custom provenance_license sources of %s", tmp);
+ return g_strsplit (tmp, ",", -1);
+ }
+ return g_settings_get_strv (self->settings, "free-repos");
+}
+
+static gchar *
+gs_plugin_provenance_license_get_id (GsPluginProvenanceLicense *self)
+{
+ const gchar *tmp;
+ g_autofree gchar *url = NULL;
+
+ tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_URL");
+ if (tmp != NULL) {
+ g_debug ("using custom license generic sources of %s", tmp);
+ url = g_strdup (tmp);
+ } else {
+ url = g_settings_get_string (self->settings, "free-repos-url");
+ if (url == NULL)
+ return g_strdup ("LicenseRef-free");
+ }
+ return g_strdup_printf ("LicenseRef-free=%s", url);
+}
+
+static void
+gs_plugin_provenance_license_changed_cb (GSettings *settings,
+ const gchar *key,
+ gpointer user_data)
+{
+ GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (user_data);
+
+ if (g_strcmp0 (key, "free-repos") == 0) {
+ g_strfreev (self->sources);
+ self->sources = gs_plugin_provenance_license_get_sources (self);
+ }
+ if (g_strcmp0 (key, "free-repos-url") == 0) {
+ g_free (self->license_id);
+ self->license_id = gs_plugin_provenance_license_get_id (self);
+ }
+}
+
+static void
+gs_plugin_provenance_license_init (GsPluginProvenanceLicense *self)
+{
+ self->settings = g_settings_new ("org.gnome.software");
+ g_signal_connect (self->settings, "changed",
+ G_CALLBACK (gs_plugin_provenance_license_changed_cb), self);
+ self->sources = gs_plugin_provenance_license_get_sources (self);
+ self->license_id = gs_plugin_provenance_license_get_id (self);
+
+ /* need this set */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "provenance");
+}
+
+static void
+gs_plugin_provenance_license_dispose (GObject *object)
+{
+ GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (object);
+
+ g_clear_pointer (&self->sources, g_strfreev);
+ g_clear_pointer (&self->license_id, g_free);
+ g_clear_object (&self->settings);
+
+ G_OBJECT_CLASS (gs_plugin_provenance_license_parent_class)->dispose (object);
+}
+
+static gboolean
+refine_app (GsPluginProvenanceLicense *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *origin;
+
+ /* not required */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0)
+ return TRUE;
+
+ /* no provenance */
+ if (!gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE))
+ return TRUE;
+
+ /* nothing to search */
+ if (self->sources == NULL || self->sources[0] == NULL)
+ return TRUE;
+
+ /* simple case */
+ origin = gs_app_get_origin (app);
+ if (origin != NULL && gs_utils_strv_fnmatch (self->sources, origin))
+ gs_app_set_license (app, GS_APP_QUALITY_NORMAL, self->license_id);
+
+ return TRUE;
+}
+
+static void
+gs_plugin_provenance_license_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (plugin);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_provenance_license_refine_async);
+
+ /* nothing to do here */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* nothing to search */
+ if (self->sources == NULL || self->sources[0] == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (!refine_app (self, app, flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_provenance_license_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_provenance_license_class_init (GsPluginProvenanceLicenseClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_provenance_license_dispose;
+
+ plugin_class->refine_async = gs_plugin_provenance_license_refine_async;
+ plugin_class->refine_finish = gs_plugin_provenance_license_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_PROVENANCE_LICENSE;
+}
diff --git a/plugins/core/gs-plugin-provenance-license.h b/plugins/core/gs-plugin-provenance-license.h
new file mode 100644
index 0000000..f793d61
--- /dev/null
+++ b/plugins/core/gs-plugin-provenance-license.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_PROVENANCE_LICENSE (gs_plugin_provenance_license_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginProvenanceLicense, gs_plugin_provenance_license, GS, PLUGIN_PROVENANCE_LICENSE, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-provenance.c b/plugins/core/gs-plugin-provenance.c
new file mode 100644
index 0000000..95e940b
--- /dev/null
+++ b/plugins/core/gs-plugin-provenance.c
@@ -0,0 +1,292 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <gnome-software.h>
+
+#include "gs-plugin-provenance.h"
+
+/*
+ * SECTION:
+ * Sets the package provenance to TRUE if installed by an official
+ * software source. Also sets compulsory quirk when a required repository.
+ *
+ * This plugin executes entirely in the main thread.
+ */
+
+struct _GsPluginProvenance {
+ GsPlugin parent;
+
+ GSettings *settings;
+ GHashTable *repos; /* gchar *name ~> guint flags */
+ GPtrArray *provenance_wildcards; /* non-NULL, when have names with wildcards */
+ GPtrArray *compulsory_wildcards; /* non-NULL, when have names with wildcards */
+};
+
+G_DEFINE_TYPE (GsPluginProvenance, gs_plugin_provenance, GS_TYPE_PLUGIN)
+
+static GHashTable *
+gs_plugin_provenance_remove_by_flag (GHashTable *old_repos,
+ GsAppQuirk quirk)
+{
+ GHashTable *new_repos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ GHashTableIter iter;
+ gpointer key, value;
+ g_hash_table_iter_init (&iter, old_repos);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ guint flags = GPOINTER_TO_UINT (value);
+ flags = flags & (~quirk);
+ if (flags != 0)
+ g_hash_table_insert (new_repos, g_strdup (key), GUINT_TO_POINTER (flags));
+ }
+ return new_repos;
+}
+
+static void
+gs_plugin_provenance_add_quirks (GsApp *app,
+ guint quirks)
+{
+ if ((quirks & GS_APP_QUIRK_PROVENANCE) != 0)
+ gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
+ if ((quirks & GS_APP_QUIRK_COMPULSORY) != 0 &&
+ gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
+ gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
+}
+
+static gchar **
+gs_plugin_provenance_get_sources (GsPluginProvenance *self,
+ const gchar *key)
+{
+ const gchar *tmp;
+ tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_SOURCES");
+ if (tmp != NULL) {
+ if (g_strcmp0 (key, "required-repos") == 0)
+ return NULL;
+ g_debug ("using custom provenance sources of %s", tmp);
+ return g_strsplit (tmp, ",", -1);
+ }
+ return g_settings_get_strv (self->settings, key);
+}
+
+static void
+gs_plugin_provenance_settings_changed_cb (GSettings *settings,
+ const gchar *key,
+ gpointer user_data)
+{
+ GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (user_data);
+ GsAppQuirk quirk = GS_APP_QUIRK_NONE;
+ GPtrArray **pwildcards = NULL;
+
+ if (g_strcmp0 (key, "official-repos") == 0) {
+ quirk = GS_APP_QUIRK_PROVENANCE;
+ pwildcards = &self->provenance_wildcards;
+ } else if (g_strcmp0 (key, "required-repos") == 0) {
+ quirk = GS_APP_QUIRK_COMPULSORY;
+ pwildcards = &self->compulsory_wildcards;
+ }
+
+ if (quirk != GS_APP_QUIRK_NONE) {
+ /* The keys are stolen by the hash table, thus free only the array */
+ g_autofree gchar **repos = NULL;
+ g_autoptr(GHashTable) old_repos = self->repos;
+ g_autoptr(GPtrArray) old_wildcards = *pwildcards;
+ GHashTable *new_repos = gs_plugin_provenance_remove_by_flag (old_repos, quirk);
+ GPtrArray *new_wildcards = NULL;
+ repos = gs_plugin_provenance_get_sources (self, key);
+ for (guint ii = 0; repos && repos[ii]; ii++) {
+ gchar *repo = g_steal_pointer (&(repos[ii]));
+ if (strchr (repo, '*') ||
+ strchr (repo, '?') ||
+ strchr (repo, '[')) {
+ if (new_wildcards == NULL)
+ new_wildcards = g_ptr_array_new_with_free_func (g_free);
+ g_ptr_array_add (new_wildcards, repo);
+ } else {
+ g_hash_table_insert (new_repos, repo,
+ GUINT_TO_POINTER (quirk |
+ GPOINTER_TO_UINT (g_hash_table_lookup (new_repos, repo))));
+ }
+ }
+ if (new_wildcards != NULL)
+ g_ptr_array_add (new_wildcards, NULL);
+ self->repos = new_repos;
+ *pwildcards = new_wildcards;
+ }
+}
+
+static void
+gs_plugin_provenance_init (GsPluginProvenance *self)
+{
+ self->settings = g_settings_new ("org.gnome.software");
+ self->repos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ g_signal_connect (self->settings, "changed",
+ G_CALLBACK (gs_plugin_provenance_settings_changed_cb), self);
+ gs_plugin_provenance_settings_changed_cb (self->settings, "official-repos", self);
+ gs_plugin_provenance_settings_changed_cb (self->settings, "required-repos", self);
+
+ /* after the package source is set */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "dummy");
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "packagekit");
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree");
+}
+
+static void
+gs_plugin_provenance_dispose (GObject *object)
+{
+ GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (object);
+
+ g_clear_pointer (&self->repos, g_hash_table_unref);
+ g_clear_pointer (&self->provenance_wildcards, g_ptr_array_unref);
+ g_clear_pointer (&self->compulsory_wildcards, g_ptr_array_unref);
+ g_clear_object (&self->settings);
+
+ G_OBJECT_CLASS (gs_plugin_provenance_parent_class)->dispose (object);
+}
+
+static gboolean
+gs_plugin_provenance_find_repo_flags (GHashTable *repos,
+ GPtrArray *provenance_wildcards,
+ GPtrArray *compulsory_wildcards,
+ const gchar *repo,
+ guint *out_flags)
+{
+ if (repo == NULL || *repo == '\0')
+ return FALSE;
+ *out_flags = GPOINTER_TO_UINT (g_hash_table_lookup (repos, repo));
+ if (provenance_wildcards != NULL &&
+ gs_utils_strv_fnmatch ((gchar **) provenance_wildcards->pdata, repo))
+ *out_flags |= GS_APP_QUIRK_PROVENANCE;
+ if (compulsory_wildcards != NULL &&
+ gs_utils_strv_fnmatch ((gchar **) compulsory_wildcards->pdata, repo))
+ *out_flags |= GS_APP_QUIRK_COMPULSORY;
+ return *out_flags != 0;
+}
+
+static gboolean
+refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GHashTable *repos,
+ GPtrArray *provenance_wildcards,
+ GPtrArray *compulsory_wildcards,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *origin;
+ guint quirks;
+
+ /* not required */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0)
+ return TRUE;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE))
+ return TRUE;
+
+ /* Software sources/repositories are represented as #GsApps too. Add the
+ * provenance quirk to the system-configured repositories (but not
+ * user-configured ones). */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) {
+ if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, gs_app_get_id (app), &quirks) &&
+ gs_app_get_scope (app) != AS_COMPONENT_SCOPE_USER)
+ gs_plugin_provenance_add_quirks (app, quirks);
+ return TRUE;
+ }
+
+ /* simple case */
+ origin = gs_app_get_origin (app);
+ if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, origin, &quirks)) {
+ gs_plugin_provenance_add_quirks (app, quirks);
+ return TRUE;
+ }
+
+ /* this only works for packages */
+ origin = gs_app_get_source_id_default (app);
+ if (origin == NULL)
+ return TRUE;
+ origin = g_strrstr (origin, ";");
+ if (origin == NULL)
+ return TRUE;
+ if (g_str_has_prefix (origin + 1, "installed:"))
+ origin += 10;
+ if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, origin + 1, &quirks))
+ gs_plugin_provenance_add_quirks (app, quirks);
+
+ return TRUE;
+}
+
+static void
+gs_plugin_provenance_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (plugin);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GHashTable) repos = NULL;
+ g_autoptr(GPtrArray) provenance_wildcards = NULL;
+ g_autoptr(GPtrArray) compulsory_wildcards = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_provenance_refine_async);
+
+ /* nothing to do here */
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ repos = g_hash_table_ref (self->repos);
+ provenance_wildcards = self->provenance_wildcards != NULL ? g_ptr_array_ref (self->provenance_wildcards) : NULL;
+ compulsory_wildcards = self->compulsory_wildcards != NULL ? g_ptr_array_ref (self->compulsory_wildcards) : NULL;
+
+ /* nothing to search */
+ if (g_hash_table_size (repos) == 0 && provenance_wildcards == NULL && compulsory_wildcards == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (!refine_app (plugin, app, flags, repos, provenance_wildcards, compulsory_wildcards, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_provenance_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_provenance_class_init (GsPluginProvenanceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_provenance_dispose;
+
+ plugin_class->refine_async = gs_plugin_provenance_refine_async;
+ plugin_class->refine_finish = gs_plugin_provenance_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_PROVENANCE;
+}
diff --git a/plugins/core/gs-plugin-provenance.h b/plugins/core/gs-plugin-provenance.h
new file mode 100644
index 0000000..e3e2d34
--- /dev/null
+++ b/plugins/core/gs-plugin-provenance.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_PROVENANCE (gs_plugin_provenance_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginProvenance, gs_plugin_provenance, GS, PLUGIN_PROVENANCE, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-plugin-rewrite-resource.c b/plugins/core/gs-plugin-rewrite-resource.c
new file mode 100644
index 0000000..44348c5
--- /dev/null
+++ b/plugins/core/gs-plugin-rewrite-resource.c
@@ -0,0 +1,252 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <gnome-software.h>
+
+#include "gs-plugin-rewrite-resource.h"
+
+/*
+ * SECTION:
+ * Rewrites CSS metadata for apps to refer to locally downloaded resources.
+ *
+ * This plugin rewrites the CSS of apps to refer to locally cached resources,
+ * rather than HTTP/HTTPS URIs for images (for example).
+ *
+ * It uses a worker thread to download the resources.
+ *
+ * FIXME: Eventually this should move into the refine plugin job, as it needs
+ * to execute after all other refine jobs (in order to see all the URIs which
+ * they produce).
+ */
+
+struct _GsPluginRewriteResource
+{
+ GsPlugin parent;
+
+ GsWorkerThread *worker; /* (owned) */
+};
+
+G_DEFINE_TYPE (GsPluginRewriteResource, gs_plugin_rewrite_resource, GS_TYPE_PLUGIN)
+
+#define assert_in_worker(self) \
+ g_assert (gs_worker_thread_is_in_worker_context (self->worker))
+
+static void
+gs_plugin_rewrite_resource_init (GsPluginRewriteResource *self)
+{
+ /* let appstream add metadata first */
+ gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+}
+
+static void
+gs_plugin_rewrite_resource_dispose (GObject *object)
+{
+ GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (object);
+
+ g_clear_object (&self->worker);
+
+ G_OBJECT_CLASS (gs_plugin_rewrite_resource_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_rewrite_resource_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_rewrite_resource_setup_async);
+
+ /* Start up a worker thread to process all the plugin’s function calls. */
+ self->worker = gs_worker_thread_new ("gs-plugin-rewrite-resource");
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_rewrite_resource_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_rewrite_resource_shutdown_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_rewrite_resource_shutdown_async);
+
+ /* Stop the worker thread. */
+ gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
+}
+
+static void
+shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = G_TASK (user_data);
+ GsPluginRewriteResource *self = g_task_get_source_object (task);
+ g_autoptr(GsWorkerThread) worker = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ worker = g_steal_pointer (&self->worker);
+
+ if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_rewrite_resource_shutdown_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static gboolean
+refine_app (GsPluginRewriteResource *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *keys[] = {
+ "GnomeSoftware::FeatureTile-css",
+ "GnomeSoftware::UpgradeBanner-css",
+ NULL };
+
+ assert_in_worker (self);
+
+ /* rewrite URIs */
+ for (guint i = 0; keys[i] != NULL; i++) {
+ const gchar *css = gs_app_get_metadata_item (app, keys[i]);
+ if (css != NULL) {
+ g_autofree gchar *css_new = NULL;
+ g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (GS_PLUGIN (self)));
+ gs_app_set_summary_missing (app_dl,
+ /* TRANSLATORS: status text when downloading */
+ _("Downloading featured images…"));
+ css_new = gs_plugin_download_rewrite_resource (GS_PLUGIN (self),
+ app,
+ css,
+ cancellable,
+ error);
+ if (css_new == NULL)
+ return FALSE;
+ if (g_strcmp0 (css, css_new) != 0) {
+ gs_app_set_metadata (app, keys[i], NULL);
+ gs_app_set_metadata (app, keys[i], css_new);
+ }
+ }
+ }
+ return TRUE;
+}
+
+static void refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_rewrite_resource_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_rewrite_resource_refine_async);
+
+ /* Queue a job for the refine. */
+ gs_worker_thread_queue (self->worker, interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW,
+ refine_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (source_object);
+ GsPluginRefineData *data = task_data;
+ GsAppList *list = data->list;
+ GsPluginRefineFlags flags = data->flags;
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ if (!refine_app (self, app, flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_rewrite_resource_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_rewrite_resource_class_init (GsPluginRewriteResourceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_rewrite_resource_dispose;
+
+ plugin_class->setup_async = gs_plugin_rewrite_resource_setup_async;
+ plugin_class->setup_finish = gs_plugin_rewrite_resource_setup_finish;
+ plugin_class->shutdown_async = gs_plugin_rewrite_resource_shutdown_async;
+ plugin_class->shutdown_finish = gs_plugin_rewrite_resource_shutdown_finish;
+ plugin_class->refine_async = gs_plugin_rewrite_resource_refine_async;
+ plugin_class->refine_finish = gs_plugin_rewrite_resource_refine_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_REWRITE_RESOURCE;
+}
diff --git a/plugins/core/gs-plugin-rewrite-resource.h b/plugins/core/gs-plugin-rewrite-resource.h
new file mode 100644
index 0000000..0af8c88
--- /dev/null
+++ b/plugins/core/gs-plugin-rewrite-resource.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_REWRITE_RESOURCE (gs_plugin_rewrite_resource_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginRewriteResource, gs_plugin_rewrite_resource, GS, PLUGIN_REWRITE_RESOURCE, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/core/gs-self-test.c b/plugins/core/gs-self-test.c
new file mode 100644
index 0000000..8266a4c
--- /dev/null
+++ b/plugins/core/gs-self-test.c
@@ -0,0 +1,277 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Joaquim Rocha <jrocha@endlessm.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+
+#include "gnome-software-private.h"
+
+#include "gs-appstream.h"
+#include "gs-test.h"
+
+const gchar * const allowlist[] = {
+ "appstream",
+ "generic-updates",
+ "icons",
+ "os-release",
+ NULL
+};
+
+static void
+gs_plugins_core_search_repo_name_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app_tmp = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GsAppQuery) query = NULL;
+ const gchar *keywords[2] = { NULL, };
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* force this app to be installed */
+ app_tmp = gs_plugin_loader_app_create (plugin_loader, "*/*/yellow/arachne.desktop/*", NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (app_tmp);
+ gs_app_set_state (app_tmp, GS_APP_STATE_INSTALLED);
+
+ /* get search result based on addon keyword */
+ keywords[0] = "yellow";
+ query = gs_app_query_new ("keywords", keywords,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT,
+ "sort-func", gs_utils_app_sort_match_value,
+ NULL);
+ plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE);
+
+ list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_nonnull (list);
+
+ /* make sure there is at least one entry, the parent app */
+ g_assert_cmpint (gs_app_list_length (list), >=, 1);
+ app = gs_app_list_index (list, 0);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "arachne.desktop");
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+}
+
+static void
+gs_plugins_core_os_release_func (GsPluginLoader *plugin_loader)
+{
+ gboolean ret;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app3 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GError) error = NULL;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* refine system application */
+ app = gs_plugin_loader_get_system_app (plugin_loader, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (app);
+ plugin_job = gs_plugin_job_refine_new_for_app (app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* make sure there is valid content */
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.fedoraproject.fedora-25");
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_OPERATING_SYSTEM);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_name (app), ==, "Fedora");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "25");
+ g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==,
+ "https://fedoraproject.org/");
+ g_assert_cmpstr (gs_app_get_metadata_item (app, "GnomeSoftware::CpeName"), ==,
+ "cpe:/o:fedoraproject:fedora:25");
+
+ /* this comes from appstream */
+ g_assert_cmpstr (gs_app_get_summary (app), ==, "Fedora Workstation");
+
+ /* check we can get this by the old name too */
+ app3 = gs_plugin_loader_get_system_app (plugin_loader, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (app3);
+ g_assert_true (app3 == app);
+}
+
+static void
+gs_plugins_core_generic_updates_func (GsPluginLoader *plugin_loader)
+{
+ gboolean ret;
+ GsApp *os_update;
+ GsAppList *related;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GsPluginJob) plugin_job2 = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app1 = NULL;
+ g_autoptr(GsApp) app2 = NULL;
+ g_autoptr(GsApp) app_wildcard = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) list_wildcard = NULL;
+ GsAppList *result_list;
+ GsAppList *result_list_wildcard;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* create a list with generic apps */
+ list = gs_app_list_new ();
+ app1 = gs_app_new ("package1");
+ app2 = gs_app_new ("package2");
+ gs_app_set_kind (app1, AS_COMPONENT_KIND_GENERIC);
+ gs_app_set_kind (app2, AS_COMPONENT_KIND_GENERIC);
+ gs_app_set_bundle_kind (app1, AS_BUNDLE_KIND_PACKAGE);
+ gs_app_set_bundle_kind (app2, AS_BUNDLE_KIND_PACKAGE);
+ gs_app_set_scope (app1, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_set_scope (app2, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_set_state (app1, GS_APP_STATE_UPDATABLE);
+ gs_app_set_state (app2, GS_APP_STATE_UPDATABLE);
+ gs_app_add_source (app1, "package1");
+ gs_app_add_source (app2, "package2");
+ gs_app_list_add (list, app1);
+ gs_app_list_add (list, app2);
+
+ /* refine to make the generic-updates plugin merge them into a single OsUpdate item */
+ plugin_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* make sure there is one entry, the os update */
+ result_list = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job));
+ g_assert_cmpint (gs_app_list_length (result_list), ==, 1);
+ os_update = gs_app_list_index (result_list, 0);
+
+ /* make sure the os update is valid */
+ g_assert_cmpstr (gs_app_get_id (os_update), ==, "org.gnome.Software.OsUpdate");
+ g_assert_cmpint (gs_app_get_kind (os_update), ==, AS_COMPONENT_KIND_GENERIC);
+ g_assert_cmpint (gs_app_get_special_kind (os_update), ==, GS_APP_SPECIAL_KIND_OS_UPDATE);
+ g_assert_true (gs_app_has_quirk (os_update, GS_APP_QUIRK_IS_PROXY));
+
+ /* must have two related apps, the ones we added earlier */
+ related = gs_app_get_related (os_update);
+ g_assert_cmpint (gs_app_list_length (related), ==, 2);
+
+ /* another test to make sure that we don't get an OsUpdate item created for wildcard apps */
+ list_wildcard = gs_app_list_new ();
+ app_wildcard = gs_app_new ("nosuchapp.desktop");
+ gs_app_add_quirk (app_wildcard, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_set_kind (app_wildcard, AS_COMPONENT_KIND_GENERIC);
+ gs_app_list_add (list_wildcard, app_wildcard);
+ plugin_job2 = gs_plugin_job_refine_new (list_wildcard, GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job2, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ result_list_wildcard = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job2));
+
+ /* no OsUpdate item created */
+ for (guint i = 0; i < gs_app_list_length (result_list_wildcard); i++) {
+ GsApp *app_tmp = gs_app_list_index (result_list_wildcard, i);
+ g_assert_cmpint (gs_app_get_kind (app_tmp), !=, AS_COMPONENT_KIND_GENERIC);
+ g_assert_cmpint (gs_app_get_special_kind (app_tmp), !=, GS_APP_SPECIAL_KIND_OS_UPDATE);
+ g_assert_false (gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_PROXY));
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_autofree gchar *tmp_root = NULL;
+ gboolean ret;
+ int retval;
+ g_autofree gchar *os_release_filename = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsPluginLoader) plugin_loader = NULL;
+ const gchar *xml;
+
+ /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories
+ * for each of the tests, we want to use the system MIME registry, assuming
+ * that it exists and correctly has shared-mime-info installed. */
+ g_content_type_set_mime_dirs (NULL);
+
+ /* Similarly, add the system-wide icon theme path before it’s
+ * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */
+ gs_test_expose_icon_theme_paths ();
+
+ gs_test_init (&argc, &argv);
+
+ /* Use a common cache directory for all tests, since the appstream
+ * plugin uses it and cannot be reinitialised for each test. */
+ tmp_root = g_dir_make_tmp ("gnome-software-core-test-XXXXXX", NULL);
+ g_assert_nonnull (tmp_root);
+ g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE);
+
+ os_release_filename = gs_test_get_filename (TESTDATADIR, "os-release");
+ g_assert_nonnull (os_release_filename);
+ g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", os_release_filename, TRUE);
+
+ /* fake some data */
+ xml = "<?xml version=\"1.0\"?>\n"
+ "<components origin=\"yellow\" version=\"0.9\">\n"
+ " <component type=\"desktop\">\n"
+ " <id>arachne.desktop</id>\n"
+ " <name>test</name>\n"
+ " <summary>Test</summary>\n"
+ " <icon type=\"stock\">system-file-manager</icon>\n"
+ " <pkgname>arachne</pkgname>\n"
+ " </component>\n"
+ " <component type=\"os-upgrade\">\n"
+ " <id>org.fedoraproject.fedora-25</id>\n"
+ " <name>Fedora</name>\n"
+ " <summary>Fedora Workstation</summary>\n"
+ " <pkgname>fedora-release</pkgname>\n"
+ " </component>\n"
+ " <info>\n"
+ " <scope>user</scope>\n"
+ " </info>\n"
+ "</components>\n";
+ g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE);
+
+ /* we can only load this once per process */
+ plugin_loader = gs_plugin_loader_new (NULL, NULL);
+ gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR);
+ ret = gs_plugin_loader_setup (plugin_loader,
+ allowlist,
+ NULL,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* plugin tests go here */
+ g_test_add_data_func ("/gnome-software/plugins/core/search-repo-name",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_core_search_repo_name_func);
+ g_test_add_data_func ("/gnome-software/plugins/core/os-release",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_core_os_release_func);
+ g_test_add_data_func ("/gnome-software/plugins/core/generic-updates",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_core_generic_updates_func);
+ retval = g_test_run ();
+
+ /* Clean up. */
+ gs_utils_rmtree (tmp_root, NULL);
+
+ return retval;
+}
diff --git a/plugins/core/meson.build b/plugins/core/meson.build
new file mode 100644
index 0000000..ca11ade
--- /dev/null
+++ b/plugins/core/meson.build
@@ -0,0 +1,133 @@
+cargs = ['-DG_LOG_DOMAIN="GsPlugin"']
+
+shared_module(
+ 'gs_plugin_generic-updates',
+ sources : 'gs-plugin-generic-updates.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+shared_module(
+ 'gs_plugin_provenance',
+ sources : 'gs-plugin-provenance.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+shared_module(
+ 'gs_plugin_provenance-license',
+ sources : 'gs-plugin-provenance-license.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+
+shared_module(
+ 'gs_plugin_icons',
+ sources : 'gs-plugin-icons.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+shared_module(
+ 'gs_plugin_appstream',
+ sources : [
+ 'gs-plugin-appstream.c'
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : [
+ plugin_libs,
+ libxmlb,
+ ],
+)
+
+shared_module(
+ 'gs_plugin_hardcoded-blocklist',
+ sources : 'gs-plugin-hardcoded-blocklist.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+shared_module(
+ 'gs_plugin_rewrite-resource',
+ sources : 'gs-plugin-rewrite-resource.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+shared_module(
+ 'gs_plugin_os-release',
+ sources : 'gs-plugin-os-release.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)
+
+if get_option('tests')
+ cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
+ cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"']
+ e = executable(
+ 'gs-self-test-core',
+ compiled_schemas,
+ sources : [
+ 'gs-self-test.c',
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ dependencies : [
+ plugin_libs,
+ libxmlb,
+ ],
+ c_args : cargs,
+ )
+ test('gs-self-test-core', e, suite: ['plugins', 'core'], env: test_env)
+endif
diff --git a/plugins/core/tests/os-release b/plugins/core/tests/os-release
new file mode 120000
index 0000000..1efe264
--- /dev/null
+++ b/plugins/core/tests/os-release
@@ -0,0 +1 @@
+../../../data/tests/os-release \ No newline at end of file