[gnome-software/1649-support-appstream-merging] gs-appstream: Add gs_appstream_add_data_merge_fixup()
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/1649-support-appstream-merging] gs-appstream: Add gs_appstream_add_data_merge_fixup()
- Date: Thu, 31 Mar 2022 10:26:15 +0000 (UTC)
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]