[gnome-software/1649-support-appstream-merging] gs-appstream: Add gs_appstream_add_data_merge_fixup()



commit 1a99bd331eb72446933e42471c610e62f15eb9dd
Author: Milan Crha <mcrha redhat com>
Date:   Thu Mar 31 12:23:06 2022 +0200

    gs-appstream: Add gs_appstream_add_data_merge_fixup()
    
    This can be used to merge component data and information from
    the .desktop files into the corresponding components.
    
    Related to https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1649

 lib/gs-appstream.c | 532 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 lib/gs-appstream.h |   4 +
 2 files changed, 536 insertions(+)
---
diff --git a/lib/gs-appstream.c b/lib/gs-appstream.c
index c399379a0..8b486a9f1 100644
--- a/lib/gs-appstream.c
+++ b/lib/gs-appstream.c
@@ -13,10 +13,16 @@
 #include <gnome-software.h>
 #include <locale.h>
 
+#include "gs-external-appstream-utils.h"
 #include "gs-appstream.h"
 
 #define        GS_APPSTREAM_MAX_SCREENSHOTS    5
 
+/* This is waiting for https://github.com/hughsie/libxmlb/issues/120
+ * The libxmlb crashes when all nodes are marked for a removal in the fixup-s
+#define FIXED_LIBXMLB 1
+*/
+
 GsApp *
 gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GError **error)
 {
@@ -2000,6 +2006,532 @@ gs_appstream_add_current_locales (XbBuilder *builder)
                xb_builder_add_locale (builder, locales[i]);
 }
 
+static gboolean
+gs_appstream_is_merge_node (XbBuilderNode *bn)
+{
+       const gchar *merge = xb_builder_node_get_attr (bn, "merge");
+       if (merge != NULL) {
+               AsMergeKind kind = as_merge_kind_from_string (merge);
+               return kind != AS_MERGE_KIND_NONE;
+       }
+       return FALSE;
+}
+
+#ifdef FIXED_LIBXMLB
+static gboolean
+gs_appstream_remove_merge_components_cb (XbBuilderFixup *self,
+                                        XbBuilderNode *bn,
+                                        gpointer user_data,
+                                        GError **error)
+{
+       if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
+           gs_appstream_is_merge_node (bn))
+               xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
+       return TRUE;
+}
+
+static gboolean
+gs_appstream_remove_nonmerge_components_cb (XbBuilderFixup *self,
+                                           XbBuilderNode *bn,
+                                           gpointer user_data,
+                                           GError **error)
+{
+       if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
+           !gs_appstream_is_merge_node (bn))
+               xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
+       return TRUE;
+}
+#endif
+
+static GInputStream *
+gs_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);
+}
+
+static void
+gs_appstream_load_appstream_file (XbBuilder *builder,
+                                 const gchar *filename,
+                                 GCancellable *cancellable)
+{
+       g_autoptr(GFile) file = g_file_new_for_path (filename);
+       g_autoptr(GError) local_error = NULL;
+       g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+       g_autoptr(XbBuilderNode) info = NULL;
+       g_autoptr(XbBuilderFixup) fixup = NULL;
+
+       if (g_cancellable_is_cancelled (cancellable))
+               return;
+
+       /* add support for DEP-11 files */
+       xb_builder_source_add_adapter (source,
+                                      "application/x-yaml",
+                                      gs_appstream_load_dep11_cb,
+                                      NULL, NULL);
+
+       /* add source */
+       if (!xb_builder_source_load_file (source, file, XB_BUILDER_SOURCE_FLAG_NONE, cancellable, 
&local_error)) {
+               g_debug ("Failed to load appstream file '%s': %s", filename, local_error->message);
+               return;
+       }
+
+       /* 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);
+
+       #ifdef FIXED_LIBXMLB
+       fixup = xb_builder_fixup_new ("RemoveNonMergeComponents",
+                                      gs_appstream_remove_nonmerge_components_cb,
+                                      NULL, NULL);
+       xb_builder_fixup_set_max_depth (fixup, 2);
+       xb_builder_source_add_fixup (source, fixup);
+       #endif
+
+       xb_builder_import_source (builder, source);
+}
+
+static void
+gs_appstream_load_appstream_dir (XbBuilder *builder,
+                                const gchar *path,
+                                GCancellable *cancellable)
+{
+       const gchar *fn;
+       g_autoptr(GDir) dir = NULL;
+#ifdef ENABLE_EXTERNAL_APPSTREAM
+       g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software");
+       gboolean external_appstream_system_wide = g_settings_get_boolean (settings, 
"external-appstream-system-wide");
+#endif
+
+       dir = g_dir_open (path, 0, NULL);
+       if (dir == NULL)
+               return;
+       while ((fn = g_dir_read_name (dir)) != NULL && !g_cancellable_is_cancelled (cancellable)) {
+#ifdef ENABLE_EXTERNAL_APPSTREAM
+               /* Ignore our own system-installed files when
+                  external-appstream-system-wide is FALSE */
+               if (!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);
+                       gs_appstream_load_appstream_file (builder, filename, cancellable);
+               }
+       }
+}
+
+typedef struct {
+       GSList *components; /* XbNode * */
+} SiloIndexData;
+
+static SiloIndexData *
+silo_index_data_new (XbNode *node)
+{
+       SiloIndexData *sid = g_new0 (SiloIndexData, 1);
+       sid->components = g_slist_prepend (sid->components, g_object_ref (node));
+       return sid;
+}
+
+static void
+silo_index_data_free (SiloIndexData *sid)
+{
+       if (sid != NULL) {
+               g_slist_free_full (sid->components, g_object_unref);
+               g_free (sid);
+       }
+}
+
+typedef struct {
+       XbSilo *appstream_silo;
+       XbSilo *desktop_silo;
+       GHashTable *appstream_index; /* gchar *id ~> SiloIndexData * */
+       GHashTable *desktop_index; /* gchar *id ~> SiloIndexData * */
+} MergeData;
+
+static MergeData *
+merge_data_new (void)
+{
+       MergeData *md = g_new0 (MergeData, 1);
+       return md;
+}
+
+static void
+merge_data_free (MergeData *md)
+{
+       if (md == NULL)
+               return;
+
+       g_clear_pointer (&md->appstream_index, g_hash_table_unref);
+       g_clear_pointer (&md->desktop_index, g_hash_table_unref);
+       g_clear_object (&md->appstream_silo);
+       g_clear_object (&md->desktop_silo);
+       g_free (md);
+}
+
+static void
+gs_appstream_add_node_to_silo_index (GHashTable *index, /* gchar *id ~> SiloIndexData * */
+                                    GPtrArray *id_nodes, /* XbNode * */
+                                    XbNode *node)
+{
+       if (id_nodes == NULL)
+               return;
+       for (guint i = 0; i < id_nodes->len; i++) {
+               XbNode *id_node = g_ptr_array_index (id_nodes, i);
+               const gchar *id = xb_node_get_text (id_node);
+               if (id != NULL) {
+                       SiloIndexData *sid = g_hash_table_lookup (index, id);
+                       if (sid != NULL) {
+                               sid->components = g_slist_prepend (sid->components, g_object_ref (node));
+                       } else {
+                               sid = silo_index_data_new (node);
+                               g_hash_table_insert (index, g_strdup (id), sid);
+                       }
+               }
+       }
+}
+
+static void
+gs_appstream_traverse_silo_for_index (XbNode *node,
+                                     GHashTable *index,
+                                     gboolean only_merges,
+                                     gint depth)
+{
+       if (g_strcmp0 (xb_node_get_element (node), "component") == 0) {
+               g_autoptr(GPtrArray) id_nodes = NULL;
+               if (only_merges) {
+                       gboolean is_merge = FALSE;
+                       const gchar *merge = xb_node_get_attr (node, "merge");
+                       if (merge != NULL) {
+                               AsMergeKind kind = as_merge_kind_from_string (merge);
+                               is_merge = kind != AS_MERGE_KIND_NONE;
+                       }
+                       if (!is_merge)
+                               return;
+               }
+               id_nodes = xb_node_query (node, "id", 1, NULL);
+               if (id_nodes != NULL)
+                       gs_appstream_add_node_to_silo_index (index, id_nodes, node);
+               if (!only_merges) {
+                       g_clear_pointer (&id_nodes, g_ptr_array_unref);
+                       /* Handle component id rename */
+                       id_nodes = xb_node_query (node, "provides/id", 0, NULL);
+                       gs_appstream_add_node_to_silo_index (index, id_nodes, node);
+               }
+       } else if (depth < 2) {
+               XbNodeChildIter iter;
+               XbNode *child = NULL;
+               xb_node_child_iter_init (&iter, node);
+               while (xb_node_child_iter_loop (&iter, &child)) {
+                       gs_appstream_traverse_silo_for_index (child, index, only_merges, depth + 1);
+               }
+       }
+}
+
+static GHashTable * /* gchar *id ~> SiloIndexData * */
+gs_appstream_create_silo_index (XbSilo *silo,
+                               gboolean only_merges)
+{
+       GHashTable *index = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) 
silo_index_data_free);
+       for (g_autoptr(XbNode) node = xb_silo_get_root (silo); node != NULL; node_set_to_next (&node)) {
+               gs_appstream_traverse_silo_for_index (node, index, only_merges, 0);
+       }
+       return index;
+}
+
+static MergeData *
+gs_appstream_gather_merge_data (GPtrArray *appstream_paths,
+                               GPtrArray *desktop_paths,
+                               GCancellable *cancellable)
+{
+       MergeData *md = merge_data_new ();
+       g_autoptr(GPtrArray) common_appstream_paths = gs_appstream_get_appstream_data_dirs ();
+       if (appstream_paths != NULL) {
+               g_autoptr(GError) local_error = NULL;
+               g_autoptr(XbBuilder) builder = xb_builder_new ();
+               gs_appstream_add_current_locales (builder);
+               for (guint i = 0; i < appstream_paths->len; i++) {
+                       const gchar *path = g_ptr_array_index (appstream_paths, i);
+                       if (g_file_test (path, G_FILE_TEST_IS_DIR))
+                               gs_appstream_load_appstream_dir (builder, path, cancellable);
+                       else
+                               gs_appstream_load_appstream_file (builder, path, cancellable);
+                       for (guint j = 0; j < common_appstream_paths->len; j++) {
+                               if (g_strcmp0 (g_ptr_array_index (common_appstream_paths, j), path) == 0) {
+                                       g_ptr_array_remove_index (common_appstream_paths, j);
+                                       break;
+                               }
+                       }
+               }
+               for (guint i = 0; i < common_appstream_paths->len; i++) {
+                       const gchar *path = g_ptr_array_index (common_appstream_paths, i);
+                       gs_appstream_load_appstream_dir (builder, path, cancellable);
+               }
+               md->appstream_silo = xb_builder_compile (builder,
+                                                        XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
+                                                        XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+                                                        cancellable, &local_error);
+               if (md->appstream_silo != NULL) {
+                       md->appstream_index = gs_appstream_create_silo_index (md->appstream_silo, TRUE);
+               } else
+                       g_warning ("Failed to compile appstream silo: %s", local_error->message);
+       } else {
+               g_autoptr(GError) local_error = NULL;
+               g_autoptr(XbBuilder) builder = xb_builder_new ();
+               gs_appstream_add_current_locales (builder);
+               for (guint i = 0; i < common_appstream_paths->len; i++) {
+                       const gchar *path = g_ptr_array_index (common_appstream_paths, i);
+                       gs_appstream_load_appstream_dir (builder, path, cancellable);
+               }
+               md->appstream_silo = xb_builder_compile (builder,
+                                                        XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
+                                                        XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+                                                        cancellable, &local_error);
+               if (md->appstream_silo != NULL)
+                       md->appstream_index = gs_appstream_create_silo_index (md->appstream_silo, TRUE);
+               else
+                       g_warning ("Failed to compile common paths appstream silo: %s", local_error->message);
+       }
+       if (desktop_paths != NULL) {
+               g_autoptr(GError) local_error = NULL;
+               g_autoptr(XbBuilder) builder = xb_builder_new ();
+               gs_appstream_add_current_locales (builder);
+               for (guint i = 0; i < desktop_paths->len; i++) {
+                       const gchar *path = g_ptr_array_index (desktop_paths, i);
+                       gs_appstream_load_desktop_files (builder, path, cancellable, NULL);
+               }
+               md->desktop_silo = xb_builder_compile (builder,
+                                                      XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
+                                                      XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+                                                      cancellable, &local_error);
+               if (md->desktop_silo != NULL)
+                       md->desktop_index = gs_appstream_create_silo_index (md->desktop_silo, FALSE);
+               else
+                       g_warning ("Failed to compile desktop silo: %s", local_error->message);
+       }
+       return md;
+}
+
+static void
+gs_appstream_copy_attrs (XbBuilderNode *des_node,
+                        XbNode *src_node)
+{
+       XbNodeAttrIter iter;
+       const gchar *attr_name, *attr_value;
+
+       xb_node_attr_iter_init (&iter, src_node);
+       while (xb_node_attr_iter_next (&iter, &attr_name, &attr_value)) {
+               xb_builder_node_set_attr (des_node, attr_name, attr_value);
+       }
+}
+
+static void
+gs_appstream_copy_node (XbBuilderNode *des_parent,
+                       XbNode *src_node)
+{
+       g_autoptr(XbBuilderNode) new_node = NULL;
+       g_autoptr(GPtrArray) children = NULL;
+       const gchar *text;
+       new_node = xb_builder_node_new (xb_node_get_element (src_node));
+       text = xb_node_get_text (src_node);
+       if (text != NULL)
+               xb_builder_node_set_text (new_node, text, -1);
+       xb_builder_node_add_child (des_parent, new_node);
+       gs_appstream_copy_attrs (new_node, src_node);
+       children = xb_node_get_children (src_node);
+       for (guint i = 0; children && i < children->len; i++) {
+               XbNode *child = g_ptr_array_index (children, i);
+               gs_appstream_copy_node (new_node, child);
+       }
+       text = xb_node_get_tail (src_node);
+       if (text != NULL)
+               xb_builder_node_set_tail (new_node, text, -1);
+}
+
+static void
+gs_appstream_merge_component_children (XbBuilderNode *bn,
+                                      XbNode *node,
+                                      gboolean is_replace)
+{
+       g_autoptr(GHashTable) checked_elems = g_hash_table_new (g_str_hash, g_str_equal); /* gchar *name ~> 
NULL*/
+       g_autoptr(GHashTable) existing_elems = NULL;
+       g_autoptr(GPtrArray) node_children = xb_node_get_children (node);
+       if (!is_replace) {
+               GPtrArray *bn_children = xb_builder_node_get_children (bn);
+               existing_elems = g_hash_table_new (g_str_hash, g_str_equal); /* gchar *name ~> NULL*/
+               for (guint i = 0; bn_children && i < bn_children->len; i++) {
+                       XbBuilderNode *bn_child = g_ptr_array_index (bn_children, i);
+                       const gchar *elem_name = xb_builder_node_get_element (bn_child);
+                       if (elem_name)
+                               g_hash_table_add (existing_elems, (gpointer) elem_name);
+               }
+       }
+       for (guint i = 0; node_children != NULL && i < node_children->len; i++) {
+               XbNode *child = g_ptr_array_index (node_children, i);
+               const gchar *elem_name = xb_node_get_element (child);
+               if (g_strcmp0 (elem_name, "id") == 0 ||
+                   g_strcmp0 (elem_name, "info") == 0)
+                       continue;
+               if (is_replace && g_hash_table_add (checked_elems, (gpointer) elem_name)) {
+                       GPtrArray *bn_children = xb_builder_node_get_children (bn);
+                       for (guint j = 0; bn_children && j < bn_children->len; j++) {
+                               XbBuilderNode *bn_child = g_ptr_array_index (bn_children, j);
+                               if (g_strcmp0 (xb_builder_node_get_element (bn_child), elem_name) == 0)
+                                       xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
+                       }
+               } else if (!is_replace && g_hash_table_contains (existing_elems, elem_name)) {
+                       /* list of those to skip if already exist */
+                       if (g_strcmp0 (elem_name, "name") == 0 ||
+                           g_strcmp0 (elem_name, "summary") == 0 ||
+                           g_strcmp0 (elem_name, "description") == 0 ||
+                           g_strcmp0 (elem_name, "launchable") == 0)
+                               continue;
+               }
+               gs_appstream_copy_node (bn, child);
+       }
+}
+
+static gboolean
+gs_appstream_apply_merges_cb (XbBuilderFixup *self,
+                             XbBuilderNode *bn,
+                             gpointer user_data,
+                             GError **error)
+{
+       MergeData *md = user_data;
+       if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
+           !gs_appstream_is_merge_node (bn)) {
+               g_autoptr(XbBuilderNode) id_node = xb_builder_node_get_child (bn, "id", NULL);
+               if (id_node != NULL) {
+                       const gchar *id = xb_builder_node_get_text (id_node);
+                       if (id != NULL && md->appstream_index) {
+                               SiloIndexData *sid = g_hash_table_lookup (md->appstream_index, id);
+                               if (sid) {
+                                       for (GSList *link = sid->components; link != NULL; link = 
g_slist_next (link)) {
+                                               XbNode *node = link->data;
+                                               if (node) {
+                                                       const gchar *merge = xb_node_get_attr (node, "merge");
+                                                       if (merge != NULL) {
+                                                               AsMergeKind kind = as_merge_kind_from_string 
(merge);
+                                                               if (kind == AS_MERGE_KIND_REMOVE_COMPONENT) {
+                                                                       xb_builder_node_add_flag (bn, 
XB_BUILDER_NODE_FLAG_IGNORE);
+                                                                       return TRUE;
+                                                               } else if (kind == AS_MERGE_KIND_APPEND ||
+                                                                          kind == AS_MERGE_KIND_REPLACE) {
+                                                                       gs_appstream_merge_component_children 
(bn, node, kind == AS_MERGE_KIND_REPLACE);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+               if (md->desktop_index) {
+                       GPtrArray *children = xb_builder_node_get_children (bn);
+                       const gchar *desktop_id = NULL;
+                       for (guint i = 0; children != NULL && i < children->len; i++) {
+                               XbBuilderNode *child = g_ptr_array_index (children, i);
+                               if (g_strcmp0 (xb_builder_node_get_element (child), "launchable") == 0 &&
+                                   g_strcmp0 (xb_builder_node_get_attr (child, "type"), "desktop-id") == 0) {
+                                       /* Can merge, only if just one desktop-id launchable is present:
+                                          
https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Application.html#tag-dapp-launchable */
+                                       if (desktop_id != NULL) {
+                                               desktop_id = NULL;
+                                               break;
+                                       }
+                                       desktop_id = xb_builder_node_get_text (child);
+                                       if (desktop_id != NULL && *desktop_id == '\0')
+                                               desktop_id = NULL;
+                               }
+                       }
+                       if (desktop_id != NULL) {
+                               SiloIndexData *sid = g_hash_table_lookup (md->desktop_index, desktop_id);
+                               if (sid) {
+                                       for (GSList *link = sid->components; link != NULL; link = 
g_slist_next (link)) {
+                                               XbNode *node = link->data;
+                                               /* Add data from the corresponding .desktop file */
+                                               if (node != NULL)
+                                                       gs_appstream_merge_component_children (bn, node, 
FALSE);
+                                       }
+                               }
+                       }
+               }
+       }
+       return TRUE;
+}
+
+void
+gs_appstream_add_data_merge_fixup (XbBuilder *builder,
+                                  GPtrArray *appstream_paths,
+                                  GPtrArray *desktop_paths,
+                                  GCancellable *cancellable)
+{
+       #ifdef FIXED_LIBXMLB
+       g_autoptr(XbBuilderFixup) fixup1 = NULL;
+       #endif
+       g_autoptr(XbBuilderFixup) fixup2 = NULL;
+       MergeData *md;
+
+       /* First read all of the merge components and .desktop files (which will be merged as well) */
+       md = gs_appstream_gather_merge_data (appstream_paths, desktop_paths, cancellable);
+
+       #ifdef FIXED_LIBXMLB
+       /* Then drop all the merge components from the result, because they are useless when being merged */
+       fixup1 = xb_builder_fixup_new ("RemoveMergeComponents",
+                                      gs_appstream_remove_merge_components_cb,
+                                      NULL, NULL);
+       xb_builder_fixup_set_max_depth (fixup1, 2);
+       xb_builder_add_fixup (builder, fixup1);
+       #endif
+
+       /* Then apply merge data to the components */
+       fixup2 = xb_builder_fixup_new ("ApplyMerges",
+                                      gs_appstream_apply_merges_cb,
+                                      md, (GDestroyNotify) merge_data_free);
+       xb_builder_fixup_set_max_depth (fixup2, 2);
+       xb_builder_add_fixup (builder, fixup2);
+}
+
 void
 gs_appstream_component_add_keyword (XbBuilderNode *component, const gchar *str)
 {
diff --git a/lib/gs-appstream.h b/lib/gs-appstream.h
index e81f6a813..a5a61fc64 100644
--- a/lib/gs-appstream.h
+++ b/lib/gs-appstream.h
@@ -70,6 +70,10 @@ gboolean      gs_appstream_load_desktop_files        (XbBuilder      *builder,
                                                         GError         **error);
 GPtrArray      *gs_appstream_get_appstream_data_dirs   (void);
 void            gs_appstream_add_current_locales       (XbBuilder      *builder);
+void            gs_appstream_add_data_merge_fixup      (XbBuilder      *builder,
+                                                        GPtrArray      *appstream_paths,
+                                                        GPtrArray      *desktop_paths,
+                                                        GCancellable   *cancellable);
 void            gs_appstream_component_add_extra_info  (XbBuilderNode  *component);
 void            gs_appstream_component_add_keyword     (XbBuilderNode  *component,
                                                         const gchar    *str);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]