summaryrefslogtreecommitdiffstats
path: root/plugins/flatpak
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/flatpak/gs-flatpak-app.c190
-rw-r--r--plugins/flatpak/gs-flatpak-app.h65
-rw-r--r--plugins/flatpak/gs-flatpak-transaction.c761
-rw-r--r--plugins/flatpak/gs-flatpak-transaction.h31
-rw-r--r--plugins/flatpak/gs-flatpak-utils.c271
-rw-r--r--plugins/flatpak/gs-flatpak-utils.h24
-rw-r--r--plugins/flatpak/gs-flatpak.c4624
-rw-r--r--plugins/flatpak/gs-flatpak.h184
-rw-r--r--plugins/flatpak/gs-plugin-flatpak.c2326
-rw-r--r--plugins/flatpak/gs-plugin-flatpak.h22
-rw-r--r--plugins/flatpak/gs-self-test.c2003
-rw-r--r--plugins/flatpak/meson.build63
-rw-r--r--plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in12
-rw-r--r--plugins/flatpak/tests/app-extension-update/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty0
-rw-r--r--plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README1
-rw-r--r--plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml12
-rw-r--r--plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata6
-rw-r--r--plugins/flatpak/tests/app-extension/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty0
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README0
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml12
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata6
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore2
-rwxr-xr-xplugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh2
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml16
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop6
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.pngbin0 -> 334 bytes
-rw-r--r--plugins/flatpak/tests/app-extension/org.test.Chiron/metadata10
-rw-r--r--plugins/flatpak/tests/app-missing-runtime/.gitignore1
l---------plugins/flatpak/tests/app-missing-runtime/org.test.Chiron1
-rw-r--r--plugins/flatpak/tests/app-update/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore2
-rw-r--r--plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh2
-rw-r--r--plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml19
l---------plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop1
l---------plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png1
-rw-r--r--plugins/flatpak/tests/app-update/org.test.Chiron/metadata4
-rw-r--r--plugins/flatpak/tests/app-with-runtime/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore2
-rwxr-xr-xplugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh2
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml16
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop7
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.pngbin0 -> 334 bytes
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata4
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty0
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata3
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore1
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README0
-rw-r--r--plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml10
-rwxr-xr-xplugins/flatpak/tests/build.py125
-rw-r--r--plugins/flatpak/tests/chiron.flatpakbin0 -> 5452 bytes
-rw-r--r--plugins/flatpak/tests/flatpakrepos.tar.gzbin0 -> 72252 bytes
-rw-r--r--plugins/flatpak/tests/meson.build34
-rw-r--r--plugins/flatpak/tests/only-runtime/.gitignore1
l---------plugins/flatpak/tests/only-runtime/org.test.Runtime1
58 files changed, 10892 insertions, 0 deletions
diff --git a/plugins/flatpak/gs-flatpak-app.c b/plugins/flatpak/gs-flatpak-app.c
new file mode 100644
index 0000000..b59515b
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-app.c
@@ -0,0 +1,190 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gs-flatpak-app.h"
+
+const gchar *
+gs_flatpak_app_get_ref_name (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RefName");
+}
+
+const gchar *
+gs_flatpak_app_get_ref_arch (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RefArch");
+}
+
+const gchar *
+gs_flatpak_app_get_commit (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::Commit");
+}
+
+GsFlatpakAppFileKind
+gs_flatpak_app_get_file_kind (GsApp *app)
+{
+ GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::FileKind");
+ if (tmp == NULL)
+ return GS_FLATPAK_APP_FILE_KIND_UNKNOWN;
+ return g_variant_get_uint32 (tmp);
+}
+
+const gchar *
+gs_flatpak_app_get_runtime_url (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RuntimeUrl");
+}
+
+FlatpakRefKind
+gs_flatpak_app_get_ref_kind (GsApp *app)
+{
+ GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::RefKind");
+ if (tmp == NULL)
+ return FLATPAK_REF_KIND_APP;
+ return g_variant_get_uint32 (tmp);
+}
+
+const gchar *
+gs_flatpak_app_get_ref_kind_as_str (GsApp *app)
+{
+ FlatpakRefKind ref_kind = gs_flatpak_app_get_ref_kind (app);
+ if (ref_kind == FLATPAK_REF_KIND_APP)
+ return "app";
+ if (ref_kind == FLATPAK_REF_KIND_RUNTIME)
+ return "runtime";
+ return NULL;
+}
+
+const gchar *
+gs_flatpak_app_get_object_id (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::ObjectID");
+}
+
+const gchar *
+gs_flatpak_app_get_repo_gpgkey (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RepoGpgKey");
+}
+
+const gchar *
+gs_flatpak_app_get_repo_url (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RepoUrl");
+}
+
+gchar *
+gs_flatpak_app_get_ref_display (GsApp *app)
+{
+ const gchar *ref_kind_as_str = gs_flatpak_app_get_ref_kind_as_str (app);
+ const gchar *ref_name = gs_flatpak_app_get_ref_name (app);
+ const gchar *ref_arch = gs_flatpak_app_get_ref_arch (app);
+ const gchar *ref_branch = gs_app_get_branch (app);
+
+ g_return_val_if_fail (ref_kind_as_str != NULL, NULL);
+ g_return_val_if_fail (ref_name != NULL, NULL);
+ g_return_val_if_fail (ref_arch != NULL, NULL);
+ g_return_val_if_fail (ref_branch != NULL, NULL);
+
+ return g_strdup_printf ("%s/%s/%s/%s",
+ ref_kind_as_str,
+ ref_name,
+ ref_arch,
+ ref_branch);
+}
+
+void
+gs_flatpak_app_set_ref_name (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::RefName", val);
+}
+
+void
+gs_flatpak_app_set_ref_arch (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::RefArch", val);
+}
+
+void
+gs_flatpak_app_set_commit (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::Commit", val);
+}
+
+void
+gs_flatpak_app_set_file_kind (GsApp *app, GsFlatpakAppFileKind file_kind)
+{
+ g_autoptr(GVariant) tmp = g_variant_new_uint32 (file_kind);
+ gs_app_set_metadata_variant (app, "flatpak::FileKind", tmp);
+}
+
+void
+gs_flatpak_app_set_runtime_url (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::RuntimeUrl", val);
+}
+
+void
+gs_flatpak_app_set_ref_kind (GsApp *app, FlatpakRefKind ref_kind)
+{
+ g_autoptr(GVariant) tmp = g_variant_new_uint32 (ref_kind);
+ gs_app_set_metadata_variant (app, "flatpak::RefKind", tmp);
+}
+
+void
+gs_flatpak_app_set_object_id (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::ObjectID", val);
+}
+
+void
+gs_flatpak_app_set_repo_gpgkey (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::RepoGpgKey", val);
+}
+
+void
+gs_flatpak_app_set_repo_url (GsApp *app, const gchar *val)
+{
+ gs_app_set_metadata (app, "flatpak::RepoUrl", val);
+}
+
+GsApp *
+gs_flatpak_app_new (const gchar *id)
+{
+ return GS_APP (g_object_new (GS_TYPE_APP, "id", id, NULL));
+}
+
+void
+gs_flatpak_app_set_main_app_ref_name (GsApp *app, const gchar *main_app_ref)
+{
+ gs_app_set_metadata (app, "flatpak::mainApp", main_app_ref);
+}
+
+const gchar *
+gs_flatpak_app_get_main_app_ref_name (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::mainApp");
+}
+
+void
+gs_flatpak_app_set_repo_filter (GsApp *app, const gchar *filter)
+{
+ gs_app_set_metadata (app, "flatpak::RepoFilter", filter);
+}
+
+const gchar *
+gs_flatpak_app_get_repo_filter (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "flatpak::RepoFilter");
+}
diff --git a/plugins/flatpak/gs-flatpak-app.h b/plugins/flatpak/gs-flatpak-app.h
new file mode 100644
index 0000000..610c8a8
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-app.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gnome-software.h>
+#include <flatpak.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ GS_FLATPAK_APP_FILE_KIND_UNKNOWN,
+ GS_FLATPAK_APP_FILE_KIND_REPO,
+ GS_FLATPAK_APP_FILE_KIND_REF,
+ GS_FLATPAK_APP_FILE_KIND_BUNDLE,
+ GS_FLATPAK_APP_FILE_KIND_LAST,
+} GsFlatpakAppFileKind;
+
+GsApp *gs_flatpak_app_new (const gchar *id);
+
+const gchar *gs_flatpak_app_get_ref_name (GsApp *app);
+const gchar *gs_flatpak_app_get_ref_arch (GsApp *app);
+FlatpakRefKind gs_flatpak_app_get_ref_kind (GsApp *app);
+const gchar *gs_flatpak_app_get_ref_kind_as_str (GsApp *app);
+gchar *gs_flatpak_app_get_ref_display (GsApp *app);
+
+const gchar *gs_flatpak_app_get_commit (GsApp *app);
+const gchar *gs_flatpak_app_get_object_id (GsApp *app);
+const gchar *gs_flatpak_app_get_repo_gpgkey (GsApp *app);
+const gchar *gs_flatpak_app_get_repo_url (GsApp *app);
+GsFlatpakAppFileKind gs_flatpak_app_get_file_kind (GsApp *app);
+const gchar *gs_flatpak_app_get_runtime_url (GsApp *app);
+
+void gs_flatpak_app_set_ref_name (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_ref_arch (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_ref_kind (GsApp *app,
+ FlatpakRefKind ref_kind);
+
+void gs_flatpak_app_set_commit (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_object_id (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_repo_gpgkey (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_repo_url (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_file_kind (GsApp *app,
+ GsFlatpakAppFileKind file_kind);
+void gs_flatpak_app_set_runtime_url (GsApp *app,
+ const gchar *val);
+void gs_flatpak_app_set_main_app_ref_name (GsApp *app,
+ const gchar *main_app_ref);
+const gchar *gs_flatpak_app_get_main_app_ref_name (GsApp *app);
+void gs_flatpak_app_set_repo_filter (GsApp *app,
+ const gchar *filter);
+const gchar *gs_flatpak_app_get_repo_filter (GsApp *app);
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-flatpak-transaction.c b/plugins/flatpak/gs-flatpak-transaction.c
new file mode 100644
index 0000000..7377f6d
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-transaction.c
@@ -0,0 +1,761 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include "gs-flatpak-app.h"
+#include "gs-flatpak-transaction.h"
+
+struct _GsFlatpakTransaction {
+ FlatpakTransaction parent_instance;
+ GHashTable *refhash; /* ref:GsApp */
+ GError *first_operation_error;
+};
+
+enum {
+ SIGNAL_REF_TO_APP,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, FLATPAK_TYPE_TRANSACTION)
+
+static void
+gs_flatpak_transaction_finalize (GObject *object)
+{
+ GsFlatpakTransaction *self;
+ g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (object));
+ self = GS_FLATPAK_TRANSACTION (object);
+
+ g_assert (self != NULL);
+ g_hash_table_unref (self->refhash);
+ if (self->first_operation_error != NULL)
+ g_error_free (self->first_operation_error);
+
+ G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->finalize (object);
+}
+
+GsApp *
+gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, const gchar *ref)
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+ return g_hash_table_lookup (self->refhash, ref);
+}
+
+static void
+gs_flatpak_transaction_add_app_internal (GsFlatpakTransaction *self, GsApp *app)
+{
+ g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app);
+ g_hash_table_insert (self->refhash, g_steal_pointer (&ref), g_object_ref (app));
+}
+
+void
+gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, GsApp *app)
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+ gs_flatpak_transaction_add_app_internal (self, app);
+ if (gs_app_get_runtime (app) != NULL)
+ gs_flatpak_transaction_add_app_internal (self, gs_app_get_runtime (app));
+}
+
+static GsApp *
+_ref_to_app (GsFlatpakTransaction *self, const gchar *ref)
+{
+ GsApp *app = g_hash_table_lookup (self->refhash, ref);
+ if (app != NULL)
+ return g_object_ref (app);
+ g_signal_emit (self, signals[SIGNAL_REF_TO_APP], 0, ref, &app);
+
+ /* Cache the result */
+ if (app != NULL)
+ g_hash_table_insert (self->refhash, g_strdup (ref), g_object_ref (app));
+
+ return app;
+}
+
+static void
+_transaction_operation_set_app (FlatpakTransactionOperation *op, GsApp *app)
+{
+ g_object_set_data_full (G_OBJECT (op), "GsApp",
+ g_object_ref (app), (GDestroyNotify) g_object_unref);
+}
+
+static GsApp *
+_transaction_operation_get_app (FlatpakTransactionOperation *op)
+{
+ return g_object_get_data (G_OBJECT (op), "GsApp");
+}
+
+gboolean
+gs_flatpak_transaction_run (FlatpakTransaction *transaction,
+ GCancellable *cancellable,
+ GError **error)
+
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+ g_autoptr(GError) error_local = NULL;
+
+ if (!flatpak_transaction_run (transaction, cancellable, &error_local)) {
+ /* whole transaction failed; restore the state for all the apps involved */
+ g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
+ for (GList *l = ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = l->data;
+ const gchar *ref = flatpak_transaction_operation_get_ref (op);
+ g_autoptr(GsApp) app = _ref_to_app (self, ref);
+ if (app == NULL) {
+ g_warning ("failed to find app for %s", ref);
+ continue;
+ }
+ gs_app_set_state_recover (app);
+ }
+
+ if (self->first_operation_error != NULL) {
+ g_propagate_error (error, g_steal_pointer (&self->first_operation_error));
+ return FALSE;
+ } else {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+_transaction_ready (FlatpakTransaction *transaction)
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+ g_autolist(GObject) ops = NULL;
+
+ /* nothing to do */
+ ops = flatpak_transaction_get_operations (transaction);
+ if (ops == NULL)
+ return TRUE; // FIXME: error?
+ for (GList *l = ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = l->data;
+ const gchar *ref = flatpak_transaction_operation_get_ref (op);
+ g_autoptr(GsApp) app = _ref_to_app (self, ref);
+ if (app != NULL) {
+ _transaction_operation_set_app (op, app);
+ /* if we're updating a component, then mark all the apps
+ * involved to ensure updating the button state */
+ if (flatpak_transaction_operation_get_operation_type (op) == FLATPAK_TRANSACTION_OPERATION_UPDATE) {
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
+ gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ }
+ }
+
+ /* Debug dump. */
+ {
+ GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
+ g_autoptr(GString) debug_message = g_string_new ("");
+
+ g_string_append_printf (debug_message,
+ "%s: op %p, app %s (%p), download size %" G_GUINT64_FORMAT ", related-to:",
+ G_STRFUNC, op,
+ app ? gs_app_get_unique_id (app) : "?",
+ app,
+ flatpak_transaction_operation_get_download_size (op));
+ for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
+ FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
+ g_string_append_printf (debug_message,
+ "\n ├ %s (%p)", flatpak_transaction_operation_get_ref (related_to_op), related_to_op);
+ }
+ g_string_append (debug_message, "\n └ (end)");
+ g_debug ("%s", debug_message->str);
+ }
+ }
+ return TRUE;
+}
+
+typedef struct
+{
+ GsFlatpakTransaction *transaction; /* (owned) */
+ FlatpakTransactionOperation *operation; /* (owned) */
+ GsApp *app; /* (owned) */
+} ProgressData;
+
+static void
+progress_data_free (ProgressData *data)
+{
+ g_clear_object (&data->operation);
+ g_clear_object (&data->app);
+ g_clear_object (&data->transaction);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProgressData, progress_data_free)
+
+static gboolean
+op_is_related_to_op (FlatpakTransactionOperation *op,
+ FlatpakTransactionOperation *root_op)
+{
+ GPtrArray *related_to_ops; /* (element-type FlatpakTransactionOperation) */
+
+ if (op == root_op)
+ return TRUE;
+
+ related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
+ for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
+ FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
+ if (related_to_op == root_op || op_is_related_to_op (related_to_op, root_op))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static guint64
+saturated_uint64_add (guint64 a, guint64 b)
+{
+ return (a <= G_MAXUINT64 - b) ? a + b : G_MAXUINT64;
+}
+
+/*
+ * update_progress_for_op:
+ * @self: a #GsFlatpakTransaction
+ * @current_progress: progress reporting object
+ * @ops: results of calling flatpak_transaction_get_operations() on @self, for performance
+ * @current_op: the #FlatpakTransactionOperation which the @current_progress is
+ * for; this is the operation currently being run by libflatpak
+ * @root_op: the #FlatpakTransactionOperation at the root of the operation subtree
+ * to calculate progress for
+ *
+ * Calculate and update the #GsApp:progress for each app associated with
+ * @root_op in a flatpak transaction. This will include the #GsApp for the app
+ * being installed (for example), but also the #GsApps for all of its runtimes
+ * and locales, and any other dependencies of them.
+ *
+ * Each #GsApp:progress is calculated based on the sum of the progress of all
+ * the apps related to that one — so the progress for an app will factor in the
+ * progress for all its runtimes.
+ */
+static void
+update_progress_for_op (GsFlatpakTransaction *self,
+ FlatpakTransactionProgress *current_progress,
+ GList *ops,
+ FlatpakTransactionOperation *current_op,
+ FlatpakTransactionOperation *root_op)
+{
+ g_autoptr(GsApp) root_app = NULL;
+ guint64 related_prior_download_bytes = 0;
+ guint64 related_download_bytes = 0;
+ guint64 current_bytes_transferred = flatpak_transaction_progress_get_bytes_transferred (current_progress);
+ gboolean seen_current_op = FALSE, seen_root_op = FALSE;
+ gboolean root_op_skipped = flatpak_transaction_operation_get_is_skipped (root_op);
+ guint percent;
+
+ /* If @root_op is being skipped and its GsApp isn't being
+ * installed/removed, don't update the progress on it. It may be that
+ * @root_op is the runtime of an app and the app is the thing the
+ * transaction was created for.
+ */
+ if (root_op_skipped) {
+ /* _transaction_operation_set_app() is only called on non-skipped ops */
+ const gchar *ref = flatpak_transaction_operation_get_ref (root_op);
+ root_app = _ref_to_app (self, ref);
+ if (root_app == NULL) {
+ g_warning ("Couldn't find GsApp for transaction operation %s",
+ flatpak_transaction_operation_get_ref (root_op));
+ return;
+ }
+ if (gs_app_get_state (root_app) != GS_APP_STATE_INSTALLING &&
+ gs_app_get_state (root_app) != GS_APP_STATE_REMOVING)
+ return;
+ } else {
+ GsApp *unskipped_root_app = _transaction_operation_get_app (root_op);
+ if (unskipped_root_app == NULL) {
+ g_warning ("Couldn't find GsApp for transaction operation %s",
+ flatpak_transaction_operation_get_ref (root_op));
+ return;
+ }
+ root_app = g_object_ref (unskipped_root_app);
+ }
+
+ /* This relies on ops in a #FlatpakTransaction being run in the order
+ * they’re returned by flatpak_transaction_get_operations(), which is true. */
+ for (GList *l = ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = FLATPAK_TRANSACTION_OPERATION (l->data);
+ guint64 op_download_size = flatpak_transaction_operation_get_download_size (op);
+
+ if (op == current_op)
+ seen_current_op = TRUE;
+ if (op == root_op)
+ seen_root_op = TRUE;
+
+ /* Currently libflatpak doesn't return skipped ops in
+ * flatpak_transaction_get_operations(), but check just in case.
+ */
+ if (op == root_op && root_op_skipped)
+ continue;
+
+ if (op_is_related_to_op (op, root_op)) {
+ /* Saturate instead of overflowing */
+ related_download_bytes = saturated_uint64_add (related_download_bytes, op_download_size);
+ if (!seen_current_op)
+ related_prior_download_bytes = saturated_uint64_add (related_prior_download_bytes, op_download_size);
+ }
+ }
+
+ g_assert (related_prior_download_bytes <= related_download_bytes);
+ g_assert (seen_root_op || root_op_skipped);
+
+ /* Avoid overflows when converting to percent, at the cost of losing
+ * some precision in the least significant digits. */
+ if (related_prior_download_bytes > G_MAXUINT64 / 100 ||
+ current_bytes_transferred > G_MAXUINT64 / 100) {
+ related_prior_download_bytes /= 100;
+ current_bytes_transferred /= 100;
+ related_download_bytes /= 100;
+ }
+
+ /* Update the progress of @root_app. */
+ if (related_download_bytes > 0)
+ percent = ((related_prior_download_bytes * 100 / related_download_bytes) +
+ (current_bytes_transferred * 100 / related_download_bytes));
+ else
+ percent = 0;
+
+ if (gs_app_get_progress (root_app) == 100 ||
+ gs_app_get_progress (root_app) == GS_APP_PROGRESS_UNKNOWN ||
+ gs_app_get_progress (root_app) <= percent) {
+ gs_app_set_progress (root_app, percent);
+ } else {
+ g_warning ("ignoring percentage %u%% -> %u%% as going down on app %s",
+ gs_app_get_progress (root_app), percent,
+ gs_app_get_unique_id (root_app));
+ }
+}
+
+static void
+update_progress_for_op_recurse_up (GsFlatpakTransaction *self,
+ FlatpakTransactionProgress *progress,
+ GList *ops,
+ FlatpakTransactionOperation *current_op,
+ FlatpakTransactionOperation *root_op)
+{
+ GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (root_op);
+
+ /* Update progress for @root_op */
+ update_progress_for_op (self, progress, ops, current_op, root_op);
+
+ /* Update progress for ops related to @root_op, e.g. apps whose runtime is @root_op */
+ for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
+ FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
+ update_progress_for_op_recurse_up (self, progress, ops, current_op, related_to_op);
+ }
+}
+
+static void
+_transaction_progress_changed_cb (FlatpakTransactionProgress *progress,
+ gpointer user_data)
+{
+ ProgressData *data = user_data;
+ GsApp *app = data->app;
+ GsFlatpakTransaction *self = data->transaction;
+ g_autolist(FlatpakTransactionOperation) ops = NULL;
+
+ if (flatpak_transaction_progress_get_is_estimating (progress)) {
+ /* "Estimating" happens while fetching the metadata, which
+ * flatpak arbitrarily decides happens during the first 5% of
+ * each operation. At this point, no more detailed progress
+ * information is available. */
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+ return;
+ }
+
+ /* Update the progress on this app, and then do the same for each
+ * related parent app up the hierarchy. For example, @data->operation
+ * could be for a runtime which was added to the transaction because of
+ * an app — so we need to update the progress on the app too.
+ *
+ * It’s important to note that a new @data->progress is created by
+ * libflatpak for each @data->operation, and there are multiple
+ * operations in a transaction. There is no #FlatpakTransactionProgress
+ * which represents the progress of the whole transaction.
+ *
+ * There may be arbitrary many levels of related-to ops. For example,
+ * one common situation would be to install an app which needs a new
+ * runtime, and that runtime needs a locale to be installed, which would
+ * give three levels of related-to relation:
+ * locale → runtime → app → (null)
+ *
+ * In addition, libflatpak may decide to skip some operations (if they
+ * turn out to not be necessary). These skipped operations are not
+ * included in the list returned by flatpak_transaction_get_operations(),
+ * but they can be accessed via
+ * flatpak_transaction_operation_get_related_to_ops(), so have to be
+ * ignored manually.
+ */
+ ops = flatpak_transaction_get_operations (FLATPAK_TRANSACTION (self));
+ update_progress_for_op_recurse_up (self, progress, ops, data->operation, data->operation);
+}
+
+static const gchar *
+_flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType ot)
+{
+ if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL)
+ return "install";
+ if (ot == FLATPAK_TRANSACTION_OPERATION_UPDATE)
+ return "update";
+ if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
+ return "install-bundle";
+ if (ot == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
+ return "uninstall";
+ return NULL;
+}
+
+static void
+progress_data_free_closure (gpointer user_data,
+ GClosure *closure)
+{
+ progress_data_free (user_data);
+}
+
+static void
+_transaction_new_operation (FlatpakTransaction *transaction,
+ FlatpakTransactionOperation *operation,
+ FlatpakTransactionProgress *progress)
+{
+ GsApp *app;
+ g_autoptr(ProgressData) progress_data = NULL;
+
+ /* find app */
+ app = _transaction_operation_get_app (operation);
+ if (app == NULL) {
+ FlatpakTransactionOperationType ot;
+ ot = flatpak_transaction_operation_get_operation_type (operation);
+ g_warning ("failed to find app for %s during %s",
+ flatpak_transaction_operation_get_ref (operation),
+ _flatpak_transaction_operation_type_to_string (ot));
+ return;
+ }
+
+ /* report progress */
+ progress_data = g_new0 (ProgressData, 1);
+ progress_data->transaction = GS_FLATPAK_TRANSACTION (g_object_ref (transaction));
+ progress_data->app = g_object_ref (app);
+ progress_data->operation = g_object_ref (operation);
+
+ g_signal_connect_data (progress, "changed",
+ G_CALLBACK (_transaction_progress_changed_cb),
+ g_steal_pointer (&progress_data),
+ progress_data_free_closure,
+ 0 /* flags */);
+ flatpak_transaction_progress_set_update_frequency (progress, 500); /* FIXME? */
+
+ /* set app status */
+ switch (flatpak_transaction_operation_get_operation_type (operation)) {
+ case FLATPAK_TRANSACTION_OPERATION_INSTALL:
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UPDATE:
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
+ gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
+ gs_app_set_state (app, GS_APP_STATE_REMOVING);
+ break;
+ default:
+ break;
+ }
+}
+
+static gboolean
+later_op_also_related (GList *ops,
+ FlatpakTransactionOperation *current_op,
+ FlatpakTransactionOperation *related_to_current_op)
+{
+ /* Here we're determining if anything in @ops which comes after
+ * @current_op is related to @related_to_current_op and not skipped
+ * (but all @ops are not skipped so no need to check explicitly)
+ */
+ gboolean found_later_op = FALSE, seen_current_op = FALSE;
+ for (GList *l = ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = l->data;
+ GPtrArray *related_to_ops;
+ if (current_op == op) {
+ seen_current_op = TRUE;
+ continue;
+ }
+ if (!seen_current_op)
+ continue;
+
+ related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
+ for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
+ FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
+ if (related_to_op == related_to_current_op) {
+ g_assert (flatpak_transaction_operation_get_is_skipped (related_to_op));
+ found_later_op = TRUE;
+ }
+ }
+ }
+
+ return found_later_op;
+}
+
+static void
+set_skipped_related_apps_to_installed (GsFlatpakTransaction *self,
+ FlatpakTransaction *transaction,
+ FlatpakTransactionOperation *operation)
+{
+ /* It's possible the thing being updated/installed, @operation, is a
+ * related ref (e.g. extension or runtime) of an app which itself doesn't
+ * need an update and therefore won't have _transaction_operation_done()
+ * called for it directly. So we have to set the main app to installed
+ * here.
+ */
+ g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
+ GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (operation);
+
+ for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
+ FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
+ if (flatpak_transaction_operation_get_is_skipped (related_to_op)) {
+ const gchar *ref;
+ g_autoptr(GsApp) related_to_app = NULL;
+
+ /* Check that no later op is also related to related_to_op, in
+ * which case we want to let that operation finish before setting
+ * the main app to installed.
+ */
+ if (later_op_also_related (ops, operation, related_to_op))
+ continue;
+
+ ref = flatpak_transaction_operation_get_ref (related_to_op);
+ related_to_app = _ref_to_app (self, ref);
+ if (related_to_app != NULL)
+ gs_app_set_state (related_to_app, GS_APP_STATE_INSTALLED);
+ }
+ }
+}
+
+static void
+_transaction_operation_done (FlatpakTransaction *transaction,
+ FlatpakTransactionOperation *operation,
+ const gchar *commit,
+ FlatpakTransactionResult details)
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+
+ /* invalidate */
+ GsApp *app = _transaction_operation_get_app (operation);
+ if (app == NULL) {
+ g_warning ("failed to find app for %s",
+ flatpak_transaction_operation_get_ref (operation));
+ return;
+ }
+ switch (flatpak_transaction_operation_get_operation_type (operation)) {
+ case FLATPAK_TRANSACTION_OPERATION_INSTALL:
+ case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+ set_skipped_related_apps_to_installed (self, transaction, operation);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UPDATE:
+ gs_app_set_version (app, gs_app_get_update_version (app));
+ gs_app_set_update_details_markup (app, NULL);
+ gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN);
+ gs_app_set_update_version (app, NULL);
+ /* force getting the new runtime */
+ gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED);
+ /* downloaded, but not yet installed */
+ if (flatpak_transaction_get_no_deploy (transaction))
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+ else
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+ set_skipped_related_apps_to_installed (self, transaction, operation);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
+ /* we don't actually know if this app is re-installable */
+ gs_flatpak_app_set_commit (app, NULL);
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ break;
+ default:
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ break;
+ }
+}
+
+static gboolean
+_transaction_operation_error (FlatpakTransaction *transaction,
+ FlatpakTransactionOperation *operation,
+ const GError *error,
+ FlatpakTransactionErrorDetails detail)
+{
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+ FlatpakTransactionOperationType operation_type = flatpak_transaction_operation_get_operation_type (operation);
+ GsApp *app = _transaction_operation_get_app (operation);
+ const gchar *ref = flatpak_transaction_operation_get_ref (operation);
+
+ if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED)) {
+ g_debug ("skipped to %s %s: %s",
+ _flatpak_transaction_operation_type_to_string (operation_type),
+ ref,
+ error->message);
+ return TRUE; /* continue */
+ }
+
+ if (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) {
+ g_warning ("failed to %s %s (non fatal): %s",
+ _flatpak_transaction_operation_type_to_string (operation_type),
+ ref,
+ error->message);
+ return TRUE; /* continue */
+ }
+
+ if (self->first_operation_error == NULL) {
+ g_propagate_error (&self->first_operation_error,
+ g_error_copy (error));
+ if (app != NULL)
+ gs_utils_error_add_app_id (&self->first_operation_error, app);
+ }
+ return FALSE; /* stop */
+}
+
+static int
+_transaction_choose_remote_for_ref (FlatpakTransaction *transaction,
+ const char *for_ref,
+ const char *runtime_ref,
+ const char * const *remotes)
+{
+ //FIXME: do something smarter
+ return 0;
+}
+
+static void
+_transaction_end_of_lifed (FlatpakTransaction *transaction,
+ const gchar *ref,
+ const gchar *reason,
+ const gchar *rebase)
+{
+ if (rebase) {
+ g_message ("%s is end-of-life, in favor of %s", ref, rebase);
+ } else if (reason) {
+ g_message ("%s is end-of-life, with reason: %s", ref, reason);
+ }
+ //FIXME: show something in the UI
+}
+
+static gboolean
+_transaction_end_of_lifed_with_rebase (FlatpakTransaction *transaction,
+ const gchar *remote,
+ const gchar *ref,
+ const gchar *reason,
+ const gchar *rebased_to_ref,
+ const gchar **previous_ids)
+{
+ if (rebased_to_ref) {
+ g_message ("%s is end-of-life, in favor of %s", ref, rebased_to_ref);
+ } else if (reason) {
+ g_message ("%s is end-of-life, with reason: %s", ref, reason);
+ }
+
+ if (rebased_to_ref && remote) {
+ g_autoptr(GError) local_error = NULL;
+
+ if (!flatpak_transaction_add_rebase (transaction, remote, rebased_to_ref,
+ NULL, previous_ids, &local_error) ||
+ !flatpak_transaction_add_uninstall (transaction, ref, &local_error)) {
+ /* There's no way to make the whole transaction fail on
+ * this error path, so just print a warning and return
+ * FALSE, which will cause the operation on the
+ * end-of-lifed ref not to be skipped.
+ */
+ g_warning ("Failed to rebase %s to %s: %s", ref, rebased_to_ref, local_error->message);
+ return FALSE;
+ }
+
+ /* Note: A message about the rename will be shown in the UI
+ * thanks to code in gs_flatpak_refine_appstream() which
+ * sets gs_app_set_renamed_from().
+ */
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+_transaction_add_new_remote (FlatpakTransaction *transaction,
+ FlatpakTransactionRemoteReason reason,
+ const char *from_id,
+ const char *remote_name,
+ const char *url)
+{
+ /* additional applications */
+ if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO) {
+ g_debug ("configuring %s as new generic remote", url);
+ return TRUE; //FIXME?
+ }
+
+ /* runtime deps always make sense */
+ if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS) {
+ g_debug ("configuring %s as new remote for deps", url);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass);
+ object_class->finalize = gs_flatpak_transaction_finalize;
+ transaction_class->ready = _transaction_ready;
+ transaction_class->add_new_remote = _transaction_add_new_remote;
+ transaction_class->new_operation = _transaction_new_operation;
+ transaction_class->operation_done = _transaction_operation_done;
+ transaction_class->operation_error = _transaction_operation_error;
+ transaction_class->choose_remote_for_ref = _transaction_choose_remote_for_ref;
+ transaction_class->end_of_lifed = _transaction_end_of_lifed;
+ transaction_class->end_of_lifed_with_rebase = _transaction_end_of_lifed_with_rebase;
+
+ signals[SIGNAL_REF_TO_APP] =
+ g_signal_new ("ref-to-app",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL, G_TYPE_OBJECT, 1, G_TYPE_STRING);
+}
+
+static void
+gs_flatpak_transaction_init (GsFlatpakTransaction *self)
+{
+ self->refhash = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_object_unref);
+}
+
+FlatpakTransaction *
+gs_flatpak_transaction_new (FlatpakInstallation *installation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsFlatpakTransaction *self;
+ self = g_initable_new (GS_TYPE_FLATPAK_TRANSACTION,
+ cancellable, error,
+ "installation", installation,
+ NULL);
+ if (self == NULL)
+ return NULL;
+ return FLATPAK_TRANSACTION (self);
+}
diff --git a/plugins/flatpak/gs-flatpak-transaction.h b/plugins/flatpak/gs-flatpak-transaction.h
new file mode 100644
index 0000000..1cc2a07
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-transaction.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gnome-software.h>
+#include <flatpak.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_FLATPAK_TRANSACTION (gs_flatpak_transaction_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, GS, FLATPAK_TRANSACTION, FlatpakTransaction)
+
+FlatpakTransaction *gs_flatpak_transaction_new (FlatpakInstallation *installation,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction,
+ const gchar *ref);
+void gs_flatpak_transaction_add_app (FlatpakTransaction *transaction,
+ GsApp *app);
+gboolean gs_flatpak_transaction_run (FlatpakTransaction *transaction,
+ GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-flatpak-utils.c b/plugins/flatpak/gs-flatpak-utils.c
new file mode 100644
index 0000000..9675810
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-utils.c
@@ -0,0 +1,271 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+#include <ostree.h>
+
+#include <glib/gi18n.h>
+
+#include "gs-flatpak-app.h"
+#include "gs-flatpak.h"
+#include "gs-flatpak-utils.h"
+
+void
+gs_flatpak_error_convert (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return;
+
+ /* this are allowed for low-level errors */
+ if (gs_utils_error_convert_gio (perror))
+ return;
+
+ /* this are allowed for low-level errors */
+ if (gs_utils_error_convert_gdbus (perror))
+ return;
+
+ /* this are allowed for network ops */
+ if (gs_utils_error_convert_gresolver (perror))
+ return;
+
+ /* custom to this plugin */
+ if (error->domain == FLATPAK_ERROR) {
+ switch (error->code) {
+ case FLATPAK_ERROR_ALREADY_INSTALLED:
+ case FLATPAK_ERROR_NOT_INSTALLED:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ break;
+ case FLATPAK_ERROR_OUT_OF_SPACE:
+ error->code = GS_PLUGIN_ERROR_NO_SPACE;
+ break;
+ case FLATPAK_ERROR_INVALID_REF:
+ case FLATPAK_ERROR_INVALID_DATA:
+ error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
+ break;
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else if (error->domain == OSTREE_GPG_ERROR) {
+ error->code = GS_PLUGIN_ERROR_NO_SECURITY;
+ } else {
+ g_warning ("can't reliably fixup error from domain %s: %s",
+ g_quark_to_string (error->domain),
+ error->message);
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+}
+
+GsApp *
+gs_flatpak_app_new_from_remote (GsPlugin *plugin,
+ FlatpakRemote *xremote,
+ gboolean is_user)
+{
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *url = NULL;
+ g_autofree gchar *filter = NULL;
+ g_autofree gchar *description = NULL;
+ g_autofree gchar *comment = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ app = gs_flatpak_app_new (flatpak_remote_get_name (xremote));
+ gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY);
+ gs_app_set_state (app, flatpak_remote_get_disabled (xremote) ?
+ GS_APP_STATE_AVAILABLE : GS_APP_STATE_INSTALLED);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST,
+ flatpak_remote_get_name (xremote));
+ gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ gs_app_set_management_plugin (app, plugin);
+ gs_flatpak_app_set_packaging_info (app);
+ gs_app_set_scope (app, is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM);
+
+ gs_app_set_metadata (app, "GnomeSoftware::SortKey", "100");
+ gs_app_set_metadata (app, "GnomeSoftware::InstallationKind",
+ is_user ? _("User Installation") : _("System Installation"));
+ if (!is_user)
+ gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
+
+ /* title */
+ title = flatpak_remote_get_title (xremote);
+ if (title != NULL) {
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, title);
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, title);
+ }
+
+ /* origin_ui on a remote is the repo dialogue section name,
+ * not the remote title */
+ gs_app_set_origin_ui (app, _("Applications"));
+
+ description = flatpak_remote_get_description (xremote);
+ if (description != NULL)
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description);
+
+ /* url */
+ url = flatpak_remote_get_url (xremote);
+ if (url != NULL)
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
+
+ filter = flatpak_remote_get_filter (xremote);
+ if (filter != NULL)
+ gs_flatpak_app_set_repo_filter (app, filter);
+
+ comment = flatpak_remote_get_comment (xremote);
+ if (comment != NULL)
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, comment);
+
+ /* success */
+ return g_steal_pointer (&app);
+}
+
+GsApp *
+gs_flatpak_app_new_from_repo_file (GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *tmp;
+ g_autofree gchar *basename = NULL;
+ g_autofree gchar *filename = NULL;
+ g_autofree gchar *repo_comment = NULL;
+ g_autofree gchar *repo_default_branch = NULL;
+ g_autofree gchar *repo_description = NULL;
+ g_autofree gchar *repo_gpgkey = NULL;
+ g_autofree gchar *repo_homepage = NULL;
+ g_autofree gchar *repo_icon = NULL;
+ g_autofree gchar *repo_id = NULL;
+ g_autofree gchar *repo_title = NULL;
+ g_autofree gchar *repo_url = NULL;
+ g_autofree gchar *repo_filter = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* read the file */
+ kf = g_key_file_new ();
+ filename = g_file_get_path (file);
+ if (!g_key_file_load_from_file (kf, filename,
+ G_KEY_FILE_NONE,
+ &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "failed to load flatpakrepo: %s",
+ error_local->message);
+ return NULL;
+ }
+
+ /* get the ID from the basename */
+ basename = g_file_get_basename (file);
+ tmp = g_strrstr (basename, ".");
+ if (tmp != NULL)
+ *tmp = '\0';
+
+ /* ensure this is valid for flatpak */
+ if (ostree_validate_remote_name (basename, NULL)) {
+ repo_id = g_steal_pointer (&basename);
+ } else {
+ repo_id = g_str_to_ascii (basename, NULL);
+
+ for (guint i = 0; repo_id[i] != '\0'; i++) {
+ if (!g_ascii_isalnum (repo_id[i]))
+ repo_id[i] = '_';
+ }
+ }
+
+ /* create source */
+ repo_title = g_key_file_get_string (kf, "Flatpak Repo", "Title", NULL);
+ repo_url = g_key_file_get_string (kf, "Flatpak Repo", "Url", NULL);
+ if (repo_title == NULL || repo_url == NULL ||
+ repo_title[0] == '\0' || repo_url[0] == '\0') {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "not enough data in file, "
+ "expected at least Title and Url");
+ return NULL;
+ }
+
+ /* check version */
+ if (g_key_file_has_key (kf, "Flatpak Repo", "Version", NULL)) {
+ guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Repo", "Version", NULL);
+ if (ver != 1) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "unsupported version %" G_GUINT64_FORMAT, ver);
+ return NULL;
+ }
+ }
+
+ /* create source */
+ app = gs_flatpak_app_new (repo_id);
+ gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REPO);
+ gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY);
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, repo_title);
+ gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ gs_flatpak_app_set_repo_url (app, repo_url);
+ gs_app_set_origin_ui (app, repo_title);
+ gs_app_set_origin_hostname (app, repo_url);
+
+ /* user specified a URL */
+ repo_gpgkey = g_key_file_get_string (kf, "Flatpak Repo", "GPGKey", NULL);
+ if (repo_gpgkey != NULL) {
+ if (g_str_has_prefix (repo_gpgkey, "http://") ||
+ g_str_has_prefix (repo_gpgkey, "https://")) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "Base64 encoded GPGKey required, not URL");
+ return NULL;
+ }
+ gs_flatpak_app_set_repo_gpgkey (app, repo_gpgkey);
+ }
+
+ /* optional data */
+ repo_homepage = g_key_file_get_string (kf, "Flatpak Repo", "Homepage", NULL);
+ if (repo_homepage != NULL)
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, repo_homepage);
+ repo_comment = g_key_file_get_string (kf, "Flatpak Repo", "Comment", NULL);
+ if (repo_comment != NULL)
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, repo_comment);
+ repo_description = g_key_file_get_string (kf, "Flatpak Repo", "Description", NULL);
+ if (repo_description != NULL)
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, repo_description);
+ repo_default_branch = g_key_file_get_string (kf, "Flatpak Repo", "DefaultBranch", NULL);
+ if (repo_default_branch != NULL)
+ gs_app_set_branch (app, repo_default_branch);
+ repo_icon = g_key_file_get_string (kf, "Flatpak Repo", "Icon", NULL);
+ if (repo_icon != NULL &&
+ (g_str_has_prefix (repo_icon, "http:") ||
+ g_str_has_prefix (repo_icon, "https:"))) {
+ g_autoptr(GIcon) icon = gs_remote_icon_new (repo_icon);
+ gs_app_add_icon (app, icon);
+ }
+ repo_filter = g_key_file_get_string (kf, "Flatpak Repo", "Filter", NULL);
+ if (repo_filter != NULL && *repo_filter != '\0')
+ gs_flatpak_app_set_repo_filter (app, repo_filter);
+
+ /* success */
+ return g_steal_pointer (&app);
+}
+
+void
+gs_flatpak_app_set_packaging_info (GsApp *app)
+{
+ g_return_if_fail (GS_IS_APP (app));
+
+ gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_FLATPAK);
+ gs_app_set_metadata (app, "GnomeSoftware::PackagingBaseCssColor", "accent_color");
+ gs_app_set_metadata (app, "GnomeSoftware::PackagingIcon", "flatpak-symbolic");
+}
diff --git a/plugins/flatpak/gs-flatpak-utils.h b/plugins/flatpak/gs-flatpak-utils.h
new file mode 100644
index 0000000..8275828
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-utils.h
@@ -0,0 +1,24 @@
+/* -*- 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+
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#include <gnome-software.h>
+
+void gs_flatpak_error_convert (GError **perror);
+GsApp *gs_flatpak_app_new_from_remote (GsPlugin *plugin,
+ FlatpakRemote *xremote,
+ gboolean is_user);
+GsApp *gs_flatpak_app_new_from_repo_file (GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+void gs_flatpak_app_set_packaging_info (GsApp *app);
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c
new file mode 100644
index 0000000..aac55b1
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak.c
@@ -0,0 +1,4624 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com>
+ * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2016-2019 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/* Notes:
+ *
+ * All GsApp's created have management-plugin set to flatpak
+ * The GsApp:origin is the remote name, e.g. test-repo
+ *
+ * Two #FlatpakInstallation objects are kept: `installation_noninteractive` and
+ * `installation_interactive`. One has flatpak_installation_set_no_interaction()
+ * set to %TRUE, the other to %FALSE.
+ *
+ * This is because multiple #GsFlatpak operations can be ongoing with different
+ * interactive states (for example, a background refresh operation while the
+ * user is refining an app in the foreground), but the #FlatpakInstallation
+ * methods don’t support per-operation interactive state.
+ *
+ * Internally, each #FlatpakInstallation will use a separate #FlatpakDir
+ * pointing to the same repository. Those #FlatpakDirs will lock the repository
+ * when using it, so parallel operations won’t race.
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <xmlb.h>
+
+#include "gs-appstream.h"
+#include "gs-flatpak-app.h"
+#include "gs-flatpak.h"
+#include "gs-flatpak-utils.h"
+
+struct _GsFlatpak {
+ GObject parent_instance;
+ GsFlatpakFlags flags;
+ FlatpakInstallation *installation_noninteractive; /* (owned) */
+ FlatpakInstallation *installation_interactive; /* (owned) */
+ GPtrArray *installed_refs; /* must be entirely replaced rather than updated internally */
+ GMutex installed_refs_mutex;
+ GHashTable *broken_remotes;
+ GMutex broken_remotes_mutex;
+ GFileMonitor *monitor;
+ AsComponentScope scope;
+ GsPlugin *plugin;
+ XbSilo *silo;
+ GRWLock silo_lock;
+ gchar *id;
+ guint changed_id;
+ GHashTable *app_silos;
+ GMutex app_silos_mutex;
+ GHashTable *remote_title; /* gchar *remote name ~> gchar *remote title */
+ GMutex remote_title_mutex;
+ gboolean requires_full_rescan;
+ gint busy; /* (atomic) */
+ gboolean changed_while_busy;
+};
+
+G_DEFINE_TYPE (GsFlatpak, gs_flatpak, G_TYPE_OBJECT)
+
+static void
+gs_plugin_refine_item_scope (GsFlatpak *self, GsApp *app)
+{
+ if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) {
+ gboolean is_user = flatpak_installation_get_is_user (self->installation_noninteractive);
+ gs_app_set_scope (app, is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM);
+ }
+}
+
+static void
+gs_flatpak_claim_app (GsFlatpak *self, GsApp *app)
+{
+ if (!gs_app_has_management_plugin (app, NULL))
+ return;
+
+ gs_app_set_management_plugin (app, self->plugin);
+ gs_flatpak_app_set_packaging_info (app);
+
+ /* only when we have a non-temp object */
+ if ((self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY) == 0) {
+ gs_app_set_scope (app, self->scope);
+ gs_flatpak_app_set_object_id (app, gs_flatpak_get_id (self));
+ }
+}
+
+static void
+gs_flatpak_ensure_remote_title (GsFlatpak *self,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->remote_title_mutex);
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ if (g_hash_table_size (self->remote_title))
+ return;
+
+ xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, NULL);
+ if (xremotes) {
+ guint ii;
+
+ for (ii = 0; ii < xremotes->len; ii++) {
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, ii);
+
+ if (flatpak_remote_get_disabled (xremote) ||
+ !flatpak_remote_get_name (xremote))
+ continue;
+
+ g_hash_table_insert (self->remote_title, g_strdup (flatpak_remote_get_name (xremote)), flatpak_remote_get_title (xremote));
+ }
+ }
+}
+
+static void
+gs_flatpak_set_app_origin (GsFlatpak *self,
+ GsApp *app,
+ const gchar *origin,
+ FlatpakRemote *xremote,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_autofree gchar *tmp = NULL;
+ const gchar *title = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (origin != NULL);
+
+ if (xremote) {
+ tmp = flatpak_remote_get_title (xremote);
+ title = tmp;
+ } else {
+ locker = g_mutex_locker_new (&self->remote_title_mutex);
+ title = g_hash_table_lookup (self->remote_title, origin);
+ }
+
+ if (!title) {
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, NULL);
+
+ if (xremotes) {
+ guint ii;
+
+ for (ii = 0; ii < xremotes->len; ii++) {
+ FlatpakRemote *yremote = g_ptr_array_index (xremotes, ii);
+
+ if (flatpak_remote_get_disabled (yremote))
+ continue;
+
+ if (g_strcmp0 (flatpak_remote_get_name (yremote), origin) == 0) {
+ title = flatpak_remote_get_title (yremote);
+
+ if (!locker)
+ locker = g_mutex_locker_new (&self->remote_title_mutex);
+
+ /* Takes ownership of the 'title' */
+ g_hash_table_insert (self->remote_title, g_strdup (origin), (gpointer) title);
+ break;
+ }
+ }
+ }
+ }
+
+ if (g_strcmp0 (origin, "flathub-beta") == 0 ||
+ g_strcmp0 (gs_app_get_branch (app), "devel") == 0 ||
+ g_strcmp0 (gs_app_get_branch (app), "master") == 0 ||
+ (gs_app_get_branch (app) && g_str_has_suffix (gs_app_get_branch (app), "beta")))
+ gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPMENT_SOURCE);
+
+ gs_app_set_origin (app, origin);
+ gs_app_set_origin_ui (app, title);
+}
+
+static void
+gs_flatpak_claim_app_list (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive)
+{
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ /* Do not claim ownership of a wildcard app */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+
+ if (gs_app_get_origin (app))
+ gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, NULL);
+
+ gs_flatpak_claim_app (self, app);
+ }
+}
+
+static void
+gs_flatpak_set_kind_from_flatpak (GsApp *app, FlatpakRef *xref)
+{
+ if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_APP) {
+ gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP);
+ } else if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_RUNTIME) {
+ const gchar *id = gs_app_get_id (app);
+ /* this is anything that's not an app, including locales
+ * sources and debuginfo */
+ if (g_str_has_suffix (id, ".Locale")) {
+ gs_app_set_kind (app, AS_COMPONENT_KIND_LOCALIZATION);
+ } else if (g_str_has_suffix (id, ".Debug") ||
+ g_str_has_suffix (id, ".Sources") ||
+ g_str_has_prefix (id, "org.freedesktop.Platform.Icontheme.") ||
+ g_str_has_prefix (id, "org.gtk.Gtk3theme.")) {
+ gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC);
+ } else {
+ gs_app_set_kind (app, AS_COMPONENT_KIND_RUNTIME);
+ }
+ }
+}
+
+static guint
+gs_get_strv_index (const gchar * const *strv,
+ const gchar *value)
+{
+ guint ii;
+
+ for (ii = 0; strv[ii]; ii++) {
+ if (g_str_equal (strv[ii], value))
+ break;
+ }
+
+ return ii;
+}
+
+static GsAppPermissions *
+perms_from_metadata (GKeyFile *keyfile)
+{
+ char **strv;
+ char *str;
+ GsAppPermissions *permissions = gs_app_permissions_new ();
+ GsAppPermissionsFlags flags = GS_APP_PERMISSIONS_FLAGS_UNKNOWN;
+
+ strv = g_key_file_get_string_list (keyfile, "Context", "sockets", NULL, NULL);
+ if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "system-bus"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_SYSTEM_BUS;
+ if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "session-bus"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_SESSION_BUS;
+ if (strv != NULL &&
+ !g_strv_contains ((const gchar * const*)strv, "fallback-x11") &&
+ g_strv_contains ((const gchar * const*)strv, "x11"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_X11;
+ g_strfreev (strv);
+
+ strv = g_key_file_get_string_list (keyfile, "Context", "devices", NULL, NULL);
+ if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "all"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_DEVICES;
+ g_strfreev (strv);
+
+ strv = g_key_file_get_string_list (keyfile, "Context", "shared", NULL, NULL);
+ if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "network"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_NETWORK;
+ g_strfreev (strv);
+
+ strv = g_key_file_get_string_list (keyfile, "Context", "filesystems", NULL, NULL);
+ if (strv != NULL) {
+ const struct {
+ const gchar *key;
+ GsAppPermissionsFlags perm;
+ } filesystems_access[] = {
+ /* Reference: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#idm45858571325264 */
+ { "home", GS_APP_PERMISSIONS_FLAGS_HOME_FULL },
+ { "home:rw", GS_APP_PERMISSIONS_FLAGS_HOME_FULL },
+ { "home:ro", GS_APP_PERMISSIONS_FLAGS_HOME_READ },
+ { "~", GS_APP_PERMISSIONS_FLAGS_HOME_FULL },
+ { "~:rw", GS_APP_PERMISSIONS_FLAGS_HOME_FULL },
+ { "~:ro", GS_APP_PERMISSIONS_FLAGS_HOME_READ },
+ { "host", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL },
+ { "host:rw", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL },
+ { "host:ro", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ },
+ { "xdg-download", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL },
+ { "xdg-download:rw", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL },
+ { "xdg-download:ro", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ },
+ { "xdg-data/flatpak/overrides:create", GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX }
+ };
+ guint filesystems_hits = 0;
+ guint strv_len = g_strv_length (strv);
+
+ for (guint i = 0; i < G_N_ELEMENTS (filesystems_access); i++) {
+ guint index = gs_get_strv_index ((const gchar * const *) strv, filesystems_access[i].key);
+ if (index < strv_len) {
+ flags |= filesystems_access[i].perm;
+ filesystems_hits++;
+ /* Mark it as used */
+ strv[index][0] = '\0';
+ }
+ }
+
+ if ((flags & GS_APP_PERMISSIONS_FLAGS_HOME_FULL) != 0)
+ flags = flags & ~GS_APP_PERMISSIONS_FLAGS_HOME_READ;
+ if ((flags & GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL) != 0)
+ flags = flags & ~GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ;
+ if ((flags & GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL) != 0)
+ flags = flags & ~GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ;
+
+ if (strv_len > filesystems_hits) {
+ /* Cover those not being part of the above filesystem_access array */
+ const struct {
+ const gchar *prefix;
+ const gchar *title;
+ const gchar *title_subdir;
+ } filesystems_other[] = {
+ /* Reference: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#idm45858571325264 */
+ { "/", NULL, N_("System folder %s") },
+ { "home/", NULL, N_("Home subfolder %s") },
+ { "~/", NULL, N_("Home subfolder %s") },
+ { "host-os", N_("Host system folders"), NULL },
+ { "host-etc", N_("Host system configuration from /etc"), NULL },
+ { "xdg-desktop", N_("Desktop folder"), N_("Desktop subfolder %s") },
+ { "xdg-documents", N_("Documents folder"), N_("Documents subfolder %s") },
+ { "xdg-music", N_("Music folder"), N_("Music subfolder %s") },
+ { "xdg-pictures", N_("Pictures folder"), N_("Pictures subfolder %s") },
+ { "xdg-public-share", N_("Public Share folder"), N_("Public Share subfolder %s") },
+ { "xdg-videos", N_("Videos folder"), N_("Videos subfolder %s") },
+ { "xdg-templates", N_("Templates folder"), N_("Templates subfolder %s") },
+ { "xdg-cache", N_("User cache folder"), N_("User cache subfolder %s") },
+ { "xdg-config", N_("User configuration folder"), N_("User configuration subfolder %s") },
+ { "xdg-data", N_("User data folder"), N_("User data subfolder %s") },
+ { "xdg-run", N_("User runtime folder"), N_("User runtime subfolder %s") }
+ };
+
+ flags |= GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_OTHER;
+
+ for (guint j = 0; strv[j]; j++) {
+ gchar *perm = strv[j];
+ gboolean is_readonly;
+ gchar *colon;
+ guint i;
+
+ /* Already handled by the flags */
+ if (!perm[0])
+ continue;
+
+ is_readonly = g_str_has_suffix (perm, ":ro");
+ colon = strrchr (perm, ':');
+ /* modifiers are ":ro", ":rw", ":create", where ":create" is ":rw" + create
+ and ":rw" is default; treat ":create" as ":rw" */
+ if (colon) {
+ /* Completeness check */
+ if (!g_str_equal (colon, ":ro") &&
+ !g_str_equal (colon, ":rw") &&
+ !g_str_equal (colon, ":create"))
+ g_debug ("Unknown filesystem permission modifier '%s' from '%s'", colon, perm);
+ /* cut it off */
+ *colon = '\0';
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (filesystems_other); i++) {
+ if (g_str_has_prefix (perm, filesystems_other[i].prefix)) {
+ g_autofree gchar *title_tmp = NULL;
+ const gchar *slash, *title = NULL;
+ slash = strchr (perm, '/');
+ /* Catch and ignore invalid permission definitions */
+ if (slash && filesystems_other[i].title_subdir != NULL) {
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wformat-nonliteral"
+ title_tmp = g_strdup_printf (
+ _(filesystems_other[i].title_subdir),
+ slash + (slash == perm ? 0 : 1));
+ #pragma GCC diagnostic pop
+ title = title_tmp;
+ } else if (!slash && filesystems_other[i].title != NULL) {
+ title = _(filesystems_other[i].title);
+ }
+ if (title != NULL) {
+ if (is_readonly)
+ gs_app_permissions_add_filesystem_read (permissions, title);
+ else
+ gs_app_permissions_add_filesystem_full (permissions, title);
+ }
+ break;
+ }
+ }
+
+ /* Nothing matched, use a generic entry */
+ if (i == G_N_ELEMENTS (filesystems_other)) {
+ g_autofree gchar *title = g_strdup_printf (_("Filesystem access to %s"), perm);
+ if (is_readonly)
+ gs_app_permissions_add_filesystem_read (permissions, title);
+ else
+ gs_app_permissions_add_filesystem_full (permissions, title);
+ }
+ }
+ }
+ }
+ g_strfreev (strv);
+
+ str = g_key_file_get_string (keyfile, "Session Bus Policy", "ca.desrt.dconf", NULL);
+ if (str != NULL && g_str_equal (str, "talk"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_SETTINGS;
+ g_free (str);
+
+ if (!(flags & GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX)) {
+ str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.Flatpak", NULL);
+ if (str != NULL && g_str_equal (str, "talk"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX;
+ g_free (str);
+ }
+
+ if (!(flags & GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX)) {
+ str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.impl.portal.PermissionStore", NULL);
+ if (str != NULL && g_str_equal (str, "talk"))
+ flags |= GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX;
+ g_free (str);
+ }
+
+ /* no permissions set */
+ if (flags == GS_APP_PERMISSIONS_FLAGS_UNKNOWN)
+ flags = GS_APP_PERMISSIONS_FLAGS_NONE;
+
+ gs_app_permissions_set_flags (permissions, flags);
+ gs_app_permissions_seal (permissions);
+
+ return permissions;
+}
+
+static void
+gs_flatpak_set_update_permissions (GsFlatpak *self,
+ GsApp *app,
+ FlatpakInstalledRef *xref,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autoptr(GBytes) old_bytes = NULL;
+ g_autoptr(GKeyFile) old_keyfile = NULL;
+ g_autoptr(GBytes) bytes = NULL;
+ g_autoptr(GKeyFile) keyfile = NULL;
+ g_autoptr(GsAppPermissions) additional_permissions = gs_app_permissions_new ();
+ g_autoptr(GError) error_local = NULL;
+
+ old_bytes = flatpak_installed_ref_load_metadata (FLATPAK_INSTALLED_REF (xref), NULL, NULL);
+ old_keyfile = g_key_file_new ();
+ g_key_file_load_from_data (old_keyfile,
+ g_bytes_get_data (old_bytes, NULL),
+ g_bytes_get_size (old_bytes),
+ 0, NULL);
+
+ bytes = flatpak_installation_fetch_remote_metadata_sync (gs_flatpak_get_installation (self, interactive),
+ gs_app_get_origin (app),
+ FLATPAK_REF (xref),
+ cancellable,
+ &error_local);
+ if (bytes == NULL) {
+ g_debug ("Failed to get metadata for remote ‘%s’: %s",
+ gs_app_get_origin (app), error_local->message);
+ g_clear_error (&error_local);
+ gs_app_permissions_set_flags (additional_permissions, GS_APP_PERMISSIONS_FLAGS_UNKNOWN);
+ } else {
+ g_autoptr(GsAppPermissions) old_permissions = NULL;
+ g_autoptr(GsAppPermissions) new_permissions = NULL;
+ const GPtrArray *new_paths;
+
+ keyfile = g_key_file_new ();
+ g_key_file_load_from_data (keyfile,
+ g_bytes_get_data (bytes, NULL),
+ g_bytes_get_size (bytes),
+ 0, NULL);
+
+ old_permissions = perms_from_metadata (old_keyfile);
+ new_permissions = perms_from_metadata (keyfile);
+
+ gs_app_permissions_set_flags (additional_permissions,
+ gs_app_permissions_get_flags (new_permissions) &
+ ~gs_app_permissions_get_flags (old_permissions));
+
+ new_paths = gs_app_permissions_get_filesystem_read (new_permissions);
+ for (guint i = 0; new_paths && i < new_paths->len; i++) {
+ const gchar *new_path = g_ptr_array_index (new_paths, i);
+ if (!gs_app_permissions_contains_filesystem_read (old_permissions, new_path))
+ gs_app_permissions_add_filesystem_read (additional_permissions, new_path);
+ }
+
+ new_paths = gs_app_permissions_get_filesystem_full (new_permissions);
+ for (guint i = 0; new_paths && i < new_paths->len; i++) {
+ const gchar *new_path = g_ptr_array_index (new_paths, i);
+ if (!gs_app_permissions_contains_filesystem_full (old_permissions, new_path))
+ gs_app_permissions_add_filesystem_full (additional_permissions, new_path);
+ }
+ }
+
+ /* no new permissions set */
+ if (gs_app_permissions_get_flags (additional_permissions) == GS_APP_PERMISSIONS_FLAGS_UNKNOWN)
+ gs_app_permissions_set_flags (additional_permissions, GS_APP_PERMISSIONS_FLAGS_NONE);
+
+ gs_app_permissions_seal (additional_permissions);
+ gs_app_set_update_permissions (app, additional_permissions);
+
+ if (gs_app_permissions_get_flags (additional_permissions) != GS_APP_PERMISSIONS_FLAGS_NONE)
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS);
+ else
+ gs_app_remove_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS);
+}
+
+static void
+gs_flatpak_set_metadata (GsFlatpak *self, GsApp *app, FlatpakRef *xref)
+{
+ g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref));
+ guint64 installed_size = 0, download_size = 0;
+
+ /* core */
+ gs_flatpak_claim_app (self, app);
+ gs_app_set_branch (app, flatpak_ref_get_branch (xref));
+ gs_app_add_source (app, ref_tmp);
+ gs_plugin_refine_item_scope (self, app);
+
+ /* flatpak specific */
+ gs_flatpak_app_set_ref_kind (app, flatpak_ref_get_kind (xref));
+ gs_flatpak_app_set_ref_name (app, flatpak_ref_get_name (xref));
+ gs_flatpak_app_set_ref_arch (app, flatpak_ref_get_arch (xref));
+ gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (xref));
+
+ /* map the flatpak kind to the gnome-software kind */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN ||
+ gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC) {
+ gs_flatpak_set_kind_from_flatpak (app, xref);
+ }
+
+ if (FLATPAK_IS_REMOTE_REF (xref) && flatpak_remote_ref_get_eol (FLATPAK_REMOTE_REF (xref)) != NULL)
+ gs_app_set_metadata (app, "GnomeSoftware::EolReason", flatpak_remote_ref_get_eol (FLATPAK_REMOTE_REF (xref)));
+ else if (FLATPAK_IS_INSTALLED_REF (xref) && flatpak_installed_ref_get_eol (FLATPAK_INSTALLED_REF (xref)) != NULL)
+ gs_app_set_metadata (app, "GnomeSoftware::EolReason", flatpak_installed_ref_get_eol (FLATPAK_INSTALLED_REF (xref)));
+
+ if (FLATPAK_IS_REMOTE_REF (xref)) {
+ installed_size = flatpak_remote_ref_get_installed_size (FLATPAK_REMOTE_REF (xref));
+ download_size = flatpak_remote_ref_get_download_size (FLATPAK_REMOTE_REF (xref));
+ } else if (FLATPAK_IS_INSTALLED_REF (xref)) {
+ installed_size = flatpak_installed_ref_get_installed_size (FLATPAK_INSTALLED_REF (xref));
+ }
+
+ gs_app_set_size_installed (app, (installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, installed_size);
+ gs_app_set_size_download (app, (download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, download_size);
+}
+
+static GsApp *
+gs_flatpak_create_app (GsFlatpak *self,
+ const gchar *origin,
+ FlatpakRef *xref,
+ FlatpakRemote *xremote,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ GsApp *app_cached;
+ g_autoptr(GsApp) app = NULL;
+
+ /* create a temp GsApp */
+ app = gs_app_new (flatpak_ref_get_name (xref));
+ gs_flatpak_set_metadata (self, app, xref);
+ if (origin != NULL) {
+ gs_flatpak_set_app_origin (self, app, origin, xremote, interactive, cancellable);
+
+ if (!(self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY)) {
+ /* return the ref'd cached copy, only if the origin is known */
+ app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cached != NULL)
+ return app_cached;
+ }
+ }
+
+ /* fallback values */
+ if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) {
+ g_autoptr(GIcon) icon = NULL;
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL,
+ flatpak_ref_get_name (FLATPAK_REF (xref)));
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+ "Framework for applications");
+ gs_app_set_version (app, flatpak_ref_get_branch (FLATPAK_REF (xref)));
+ icon = g_themed_icon_new ("system-component-runtime");
+ gs_app_add_icon (app, icon);
+ }
+
+ /* Don't add NULL origin apps to the cache. If the app is later set to
+ * origin x the cache may return it as a match for origin y since the cache
+ * hash table uses as_utils_data_id_equal() as the equal func and a NULL
+ * origin becomes a "*" in gs_utils_build_unique_id().
+ */
+ if (origin != NULL && !(self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY))
+ gs_plugin_cache_add (self->plugin, NULL, app);
+
+ /* no existing match, just steal the temp object */
+ return g_steal_pointer (&app);
+}
+
+static GsApp *
+gs_flatpak_create_source (GsFlatpak *self, FlatpakRemote *xremote)
+{
+ GsApp *app_cached;
+ g_autoptr(GsApp) app = NULL;
+
+ /* create a temp GsApp */
+ app = gs_flatpak_app_new_from_remote (self->plugin, xremote,
+ flatpak_installation_get_is_user (self->installation_noninteractive));
+ gs_flatpak_claim_app (self, app);
+
+ /* we already have one, returned the ref'd cached copy */
+ app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cached != NULL)
+ return app_cached;
+
+ /* no existing match, just steal the temp object */
+ gs_plugin_cache_add (self->plugin, NULL, app);
+ return g_steal_pointer (&app);
+}
+
+static void
+gs_flatpak_invalidate_silo (GsFlatpak *self)
+{
+ g_rw_lock_writer_lock (&self->silo_lock);
+ if (self->silo != NULL)
+ xb_silo_invalidate (self->silo);
+ g_rw_lock_writer_unlock (&self->silo_lock);
+}
+
+static void
+gs_flatpak_internal_data_changed (GsFlatpak *self)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ /* drop the installed refs cache */
+ locker = g_mutex_locker_new (&self->installed_refs_mutex);
+ g_clear_pointer (&self->installed_refs, g_ptr_array_unref);
+ g_clear_pointer (&locker, g_mutex_locker_free);
+
+ /* drop the remote title cache */
+ locker = g_mutex_locker_new (&self->remote_title_mutex);
+ g_hash_table_remove_all (self->remote_title);
+ g_clear_pointer (&locker, g_mutex_locker_free);
+
+ /* give all the repos a second chance */
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+ g_hash_table_remove_all (self->broken_remotes);
+ g_clear_pointer (&locker, g_mutex_locker_free);
+
+ gs_flatpak_invalidate_silo (self);
+
+ self->requires_full_rescan = TRUE;
+}
+
+static gboolean
+gs_flatpak_claim_changed_idle_cb (gpointer user_data)
+{
+ GsFlatpak *self = user_data;
+
+ gs_flatpak_internal_data_changed (self);
+ gs_plugin_cache_invalidate (self->plugin);
+ gs_plugin_reload (self->plugin);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_flatpak_changed_cb (GFileMonitor *monitor,
+ GFile *child,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ GsFlatpak *self)
+{
+ if (gs_flatpak_get_busy (self)) {
+ self->changed_while_busy = TRUE;
+ } else {
+ gs_flatpak_claim_changed_idle_cb (self);
+ }
+}
+
+static gboolean
+gs_flatpak_add_flatpak_keyword_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0)
+ gs_appstream_component_add_keyword (bn, "flatpak");
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_fix_id_desktop_suffix_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ g_auto(GStrv) split = NULL;
+ g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL);
+ g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL);
+ if (id == NULL || bundle == NULL)
+ return TRUE;
+ split = g_strsplit (xb_builder_node_get_text (bundle), "/", -1);
+ if (g_strv_length (split) != 4)
+ return TRUE;
+ if (g_strcmp0 (xb_builder_node_get_text (id), split[1]) != 0) {
+ g_debug ("fixing up <id>%s</id> to %s",
+ xb_builder_node_get_text (id), split[1]);
+ gs_appstream_component_add_provide (bn, xb_builder_node_get_text (id));
+ xb_builder_node_set_text (id, split[1], -1);
+ }
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_add_bundle_tag_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const char *app_ref = (char *)user_data;
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL);
+ g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL);
+ if (id == NULL || bundle != NULL)
+ return TRUE;
+ g_debug ("adding <bundle> tag for %s", app_ref);
+ xb_builder_node_insert_text (bn, "bundle", app_ref, "type", "flatpak", NULL);
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_fix_metadata_tag_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ g_autoptr(XbBuilderNode) metadata = xb_builder_node_get_child (bn, "metadata", NULL);
+ if (metadata != NULL)
+ xb_builder_node_set_element (metadata, "custom");
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_set_origin_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const char *remote_name = (char *)user_data;
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) {
+ xb_builder_node_set_attr (bn, "origin",
+ remote_name);
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_filter_default_branch_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const gchar *default_branch = (const gchar *) user_data;
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL);
+ g_auto(GStrv) split = NULL;
+ if (bc == NULL) {
+ g_debug ("no bundle for component");
+ return TRUE;
+ }
+ split = g_strsplit (xb_builder_node_get_text (bc), "/", -1);
+ if (split == NULL || g_strv_length (split) != 4)
+ return TRUE;
+ if (g_strcmp0 (split[3], default_branch) != 0) {
+ g_debug ("not adding app with branch %s as filtering to %s",
+ split[3], default_branch);
+ xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
+ }
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_filter_noenumerate_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const gchar *main_ref = (const gchar *) user_data;
+
+ if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
+ g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL);
+ if (bc == NULL) {
+ g_debug ("no bundle for component");
+ return TRUE;
+ }
+ if (g_strcmp0 (xb_builder_node_get_text (bc), main_ref) != 0) {
+ g_debug ("not adding app %s as filtering to %s",
+ xb_builder_node_get_text (bc), main_ref);
+ xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
+ }
+ }
+ return TRUE;
+}
+
+#if LIBXMLB_CHECK_VERSION(0,3,0)
+static gboolean
+gs_flatpak_tokenize_cb (XbBuilderFixup *self,
+ XbBuilderNode *bn,
+ gpointer user_data,
+ GError **error)
+{
+ const gchar * const elements_to_tokenize[] = {
+ "id",
+ "keyword",
+ "launchable",
+ "mimetype",
+ "name",
+ "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 void
+fixup_flatpak_appstream_xml (XbBuilderSource *source,
+ const char *origin)
+{
+ g_autoptr(XbBuilderFixup) fixup1 = NULL;
+ g_autoptr(XbBuilderFixup) fixup2 = NULL;
+ g_autoptr(XbBuilderFixup) fixup3 = NULL;
+#if LIBXMLB_CHECK_VERSION(0,3,0)
+ g_autoptr(XbBuilderFixup) fixup5 = NULL;
+#endif
+
+ /* add the flatpak search keyword */
+ fixup1 = xb_builder_fixup_new ("AddKeywordFlatpak",
+ gs_flatpak_add_flatpak_keyword_cb,
+ NULL, NULL);
+ xb_builder_fixup_set_max_depth (fixup1, 2);
+ xb_builder_source_add_fixup (source, fixup1);
+
+ /* ensure the <id> matches the flatpak ref ID */
+ fixup2 = xb_builder_fixup_new ("FixIdDesktopSuffix",
+ gs_flatpak_fix_id_desktop_suffix_cb,
+ NULL, NULL);
+ xb_builder_fixup_set_max_depth (fixup2, 2);
+ xb_builder_source_add_fixup (source, fixup2);
+
+ /* Fixup <metadata> to <custom> for appstream versions >= 0.9 */
+ fixup3 = xb_builder_fixup_new ("FixMetadataTag",
+ gs_flatpak_fix_metadata_tag_cb,
+ NULL, NULL);
+ xb_builder_fixup_set_max_depth (fixup3, 2);
+ xb_builder_source_add_fixup (source, fixup3);
+
+#if LIBXMLB_CHECK_VERSION(0,3,0)
+ fixup5 = xb_builder_fixup_new ("TextTokenize",
+ gs_flatpak_tokenize_cb,
+ NULL, NULL);
+ xb_builder_fixup_set_max_depth (fixup5, 2);
+ xb_builder_source_add_fixup (source, fixup5);
+#endif
+
+ if (origin != NULL) {
+ g_autoptr(XbBuilderFixup) fixup4 = NULL;
+
+ /* override the *AppStream* origin */
+ fixup4 = xb_builder_fixup_new ("SetOrigin",
+ gs_flatpak_set_origin_cb,
+ g_strdup (origin), g_free);
+ xb_builder_fixup_set_max_depth (fixup4, 1);
+ xb_builder_source_add_fixup (source, fixup4);
+ }
+}
+
+static gboolean
+gs_flatpak_refresh_appstream_remote (GsFlatpak *self,
+ const gchar *remote_name,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+
+static gboolean
+gs_flatpak_add_apps_from_xremote (GsFlatpak *self,
+ XbBuilder *builder,
+ FlatpakRemote *xremote,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *appstream_dir_fn = NULL;
+ g_autofree gchar *appstream_fn = NULL;
+ g_autofree gchar *icon_prefix = NULL;
+ g_autofree gchar *default_branch = NULL;
+ g_autoptr(GFile) appstream_dir = NULL;
+ g_autoptr(GFile) file_xml = NULL;
+ g_autoptr(GSettings) settings = NULL;
+ g_autoptr(XbBuilderNode) info = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+ const gchar *remote_name = flatpak_remote_get_name (xremote);
+ gboolean did_refresh = FALSE;
+
+ /* get the AppStream data location */
+ appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL);
+ if (appstream_dir == NULL) {
+ g_autoptr(GError) error_local = NULL;
+ g_debug ("no appstream dir for %s, trying refresh...",
+ remote_name);
+
+ if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive, cancellable, &error_local)) {
+ g_debug ("Failed to refresh appstream data for '%s': %s", remote_name, error_local->message);
+ if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED)) {
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+
+ /* don't try to fetch this again until refresh() */
+ g_hash_table_insert (self->broken_remotes,
+ g_strdup (remote_name),
+ GUINT_TO_POINTER (1));
+ }
+ return TRUE;
+ }
+
+ appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL);
+ if (appstream_dir == NULL) {
+ g_debug ("no appstream dir for %s even after refresh, skipping",
+ remote_name);
+ return TRUE;
+ }
+
+ did_refresh = TRUE;
+ }
+
+ /* load the file into a temp silo */
+ appstream_dir_fn = g_file_get_path (appstream_dir);
+ appstream_fn = g_build_filename (appstream_dir_fn, "appstream.xml.gz", NULL);
+ if (!g_file_test (appstream_fn, G_FILE_TEST_EXISTS)) {
+ g_autoptr(GError) error_local = NULL;
+ g_debug ("no appstream metadata found for '%s' (file: %s), %s",
+ remote_name,
+ appstream_fn,
+ did_refresh ? "skipping" : "trying refresh...");
+ if (did_refresh)
+ return TRUE;
+
+ if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive, cancellable, &error_local)) {
+ g_debug ("Failed to refresh appstream data for '%s': %s", remote_name, error_local->message);
+ if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED)) {
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+
+ /* don't try to fetch this again until refresh() */
+ g_hash_table_insert (self->broken_remotes,
+ g_strdup (remote_name),
+ GUINT_TO_POINTER (1));
+ }
+ return TRUE;
+ }
+
+ if (!g_file_test (appstream_fn, G_FILE_TEST_EXISTS)) {
+ g_debug ("no appstream metadata found for '%s', even after refresh (file: %s), skipping",
+ remote_name,
+ appstream_fn);
+ return TRUE;
+ }
+ }
+
+ /* add source */
+ file_xml = g_file_new_for_path (appstream_fn);
+ if (!xb_builder_source_load_file (source, file_xml,
+ XB_BUILDER_SOURCE_FLAG_WATCH_FILE |
+ XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT,
+ cancellable,
+ error))
+ return FALSE;
+
+ fixup_flatpak_appstream_xml (source, remote_name);
+
+ /* add metadata */
+ icon_prefix = g_build_filename (appstream_dir_fn, "icons", NULL);
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL);
+ xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL);
+ xb_builder_source_set_info (source, info);
+
+ /* only add the specific app for noenumerate=true */
+ if (flatpak_remote_get_noenumerate (xremote)) {
+ g_autofree gchar *main_ref = NULL;
+
+ main_ref = flatpak_remote_get_main_ref (xremote);
+
+ if (main_ref != NULL) {
+ g_autoptr(XbBuilderFixup) fixup = NULL;
+ fixup = xb_builder_fixup_new ("FilterNoEnumerate",
+ gs_flatpak_filter_noenumerate_cb,
+ g_strdup (main_ref),
+ g_free);
+ xb_builder_fixup_set_max_depth (fixup, 2);
+ xb_builder_source_add_fixup (source, fixup);
+ }
+ }
+
+ /* do we want to filter to the default branch */
+ settings = g_settings_new ("org.gnome.software");
+ default_branch = flatpak_remote_get_default_branch (xremote);
+ if (g_settings_get_boolean (settings, "filter-default-branch") &&
+ default_branch != NULL) {
+ g_autoptr(XbBuilderFixup) fixup = NULL;
+ fixup = xb_builder_fixup_new ("FilterDefaultbranch",
+ gs_flatpak_filter_default_branch_cb,
+ flatpak_remote_get_default_branch (xremote),
+ g_free);
+ xb_builder_fixup_set_max_depth (fixup, 2);
+ xb_builder_source_add_fixup (source, fixup);
+ }
+
+ /* success */
+ xb_builder_import_source (builder, source);
+ 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_flatpak_load_desktop_fn (GsFlatpak *self,
+ XbBuilder *builder,
+ const gchar *filename,
+ const gchar *icon_prefix,
+ 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 ();
+ g_autoptr(XbBuilderFixup) fixup = NULL;
+
+ /* add support for desktop files */
+ xb_builder_source_add_adapter (source, "application/x-desktop",
+ gs_plugin_appstream_load_desktop_cb, NULL, NULL);
+
+ /* add the flatpak search keyword */
+ fixup = xb_builder_fixup_new ("AddKeywordFlatpak",
+ gs_flatpak_add_flatpak_keyword_cb,
+ self, NULL);
+ xb_builder_fixup_set_max_depth (fixup, 2);
+ xb_builder_source_add_fixup (source, fixup);
+
+ /* set the component metadata */
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL);
+ xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL);
+ xb_builder_source_set_info (source, info);
+
+ /* 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;
+ }
+
+ /* success */
+ xb_builder_import_source (builder, source);
+ return TRUE;
+}
+
+static void
+gs_flatpak_rescan_installed (GsFlatpak *self,
+ XbBuilder *builder,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *fn;
+ g_autoptr(GFile) path = NULL;
+ g_autoptr(GDir) dir = NULL;
+ g_autofree gchar *path_str = NULL;
+ g_autofree gchar *path_exports = NULL;
+ g_autofree gchar *path_apps = NULL;
+
+ /* add all installed desktop files */
+ path = flatpak_installation_get_path (self->installation_noninteractive);
+ path_str = g_file_get_path (path);
+ path_exports = g_build_filename (path_str, "exports", NULL);
+ path_apps = g_build_filename (path_exports, "share", "applications", NULL);
+ dir = g_dir_open (path_apps, 0, NULL);
+ if (dir == NULL)
+ return;
+ while ((fn = g_dir_read_name (dir)) != NULL) {
+ g_autofree gchar *filename = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* ignore */
+ if (g_strcmp0 (fn, "mimeinfo.cache") == 0)
+ continue;
+
+ /* parse desktop files */
+ filename = g_build_filename (path_apps, fn, NULL);
+ if (!gs_flatpak_load_desktop_fn (self,
+ builder,
+ filename,
+ path_exports,
+ cancellable,
+ &error_local)) {
+ g_debug ("ignoring %s: %s", filename, error_local->message);
+ continue;
+ }
+ }
+}
+
+static gboolean
+gs_flatpak_rescan_appstream_store (GsFlatpak *self,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *const *locales = g_get_language_names ();
+ g_autofree gchar *blobfn = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GPtrArray) xremotes = NULL;
+ g_autoptr(GRWLockReaderLocker) reader_locker = NULL;
+ g_autoptr(GRWLockWriterLocker) writer_locker = NULL;
+ g_autoptr(XbBuilder) builder = NULL;
+ 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]);
+
+ /* go through each remote adding metadata */
+ xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive),
+ cancellable,
+ error);
+ if (xremotes == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < xremotes->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ if (flatpak_remote_get_disabled (xremote))
+ continue;
+ g_debug ("found remote %s",
+ flatpak_remote_get_name (xremote));
+ if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, interactive, cancellable, &error_local)) {
+ g_debug ("Failed to add apps from remote ‘%s’; skipping: %s",
+ flatpak_remote_get_name (xremote), error_local->message);
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+ }
+
+ /* add any installed files without AppStream info */
+ gs_flatpak_rescan_installed (self, builder, cancellable, error);
+
+ /* create per-user cache */
+ blobfn = gs_utils_get_cache_filename (gs_flatpak_get_id (self),
+ "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,
+ cancellable, error);
+
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+
+ if (self->silo == NULL)
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_rescan_app_data (GsFlatpak *self,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ if (self->requires_full_rescan) {
+ gboolean res = gs_flatpak_refresh (self, 60, interactive, cancellable, error);
+ if (res)
+ self->requires_full_rescan = FALSE;
+ else
+ gs_flatpak_internal_data_changed (self);
+ return res;
+ }
+
+ if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) {
+ gs_flatpak_internal_data_changed (self);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* Returns with a read lock held on @self->silo_lock on success.
+ The *locker should be NULL when being called. */
+static gboolean
+ensure_flatpak_silo_with_locker (GsFlatpak *self,
+ GRWLockReaderLocker **locker,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* should not hold the lock when called */
+ g_return_val_if_fail (*locker == NULL, FALSE);
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+ return FALSE;
+
+ *locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ while (self->silo == NULL) {
+ g_clear_pointer (locker, g_rw_lock_reader_locker_free);
+
+ if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) {
+ gs_flatpak_internal_data_changed (self);
+ return FALSE;
+ }
+
+ /* At this point either rescan_appstream_store() returned an error or it successfully
+ * initialised self->silo. There is the possibility that another thread will invalidate
+ * the silo before we regain the lock. If so, we’ll have to rescan again. */
+ *locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_setup (GsFlatpak *self, GCancellable *cancellable, GError **error)
+{
+ /* watch for changes */
+ self->monitor = flatpak_installation_create_monitor (self->installation_noninteractive,
+ cancellable,
+ error);
+ if (self->monitor == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ self->changed_id =
+ g_signal_connect (self->monitor, "changed",
+ G_CALLBACK (gs_plugin_flatpak_changed_cb), self);
+
+ /* success */
+ return TRUE;
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GsApp *app;
+} GsFlatpakProgressHelper;
+
+static void
+gs_flatpak_progress_helper_free (GsFlatpakProgressHelper *phelper)
+{
+ g_object_unref (phelper->plugin);
+ if (phelper->app != NULL)
+ g_object_unref (phelper->app);
+ g_slice_free (GsFlatpakProgressHelper, phelper);
+}
+
+static GsFlatpakProgressHelper *
+gs_flatpak_progress_helper_new (GsPlugin *plugin, GsApp *app)
+{
+ GsFlatpakProgressHelper *phelper;
+ phelper = g_slice_new0 (GsFlatpakProgressHelper);
+ phelper->plugin = g_object_ref (plugin);
+ if (app != NULL)
+ phelper->app = g_object_ref (app);
+ return phelper;
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsFlatpakProgressHelper, gs_flatpak_progress_helper_free)
+
+static void
+gs_flatpak_progress_cb (const gchar *status,
+ guint progress,
+ gboolean estimating,
+ gpointer user_data)
+{
+ GsFlatpakProgressHelper *phelper = (GsFlatpakProgressHelper *) user_data;
+ GsPluginStatus plugin_status = GS_PLUGIN_STATUS_DOWNLOADING;
+
+ if (phelper->app != NULL) {
+ if (estimating)
+ gs_app_set_progress (phelper->app, GS_APP_PROGRESS_UNKNOWN);
+ else
+ gs_app_set_progress (phelper->app, progress);
+
+ switch (gs_app_get_state (phelper->app)) {
+ case GS_APP_STATE_INSTALLING:
+ plugin_status = GS_PLUGIN_STATUS_INSTALLING;
+ break;
+ case GS_APP_STATE_REMOVING:
+ plugin_status = GS_PLUGIN_STATUS_REMOVING;
+ break;
+ default:
+ break;
+ }
+ }
+ gs_plugin_status_update (phelper->plugin, phelper->app, plugin_status);
+}
+
+static gboolean
+gs_flatpak_refresh_appstream_remote (GsFlatpak *self,
+ const gchar *remote_name,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *str = NULL;
+ g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (self->plugin));
+ g_autoptr(GsFlatpakProgressHelper) phelper = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+ g_autoptr(GError) error_local = NULL;
+
+ /* TRANSLATORS: status text when downloading new metadata */
+ str = g_strdup_printf (_("Getting flatpak metadata for %s…"), remote_name);
+ gs_app_set_summary_missing (app_dl, str);
+ gs_plugin_status_update (self->plugin, app_dl, GS_PLUGIN_STATUS_DOWNLOADING);
+
+ if (!flatpak_installation_update_remote_sync (installation,
+ remote_name,
+ cancellable,
+ &error_local)) {
+ g_debug ("Failed to update metadata for remote %s: %s",
+ remote_name, error_local->message);
+ gs_flatpak_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ phelper = gs_flatpak_progress_helper_new (self->plugin, app_dl);
+ if (!flatpak_installation_update_appstream_full_sync (installation,
+ remote_name,
+ NULL, /* arch */
+ gs_flatpak_progress_cb,
+ phelper,
+ NULL, /* out_changed */
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* success */
+ gs_app_set_progress (app_dl, 100);
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_refresh_appstream (GsFlatpak *self,
+ guint64 cache_age_secs,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret;
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ /* get remotes */
+ xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive),
+ cancellable,
+ error);
+ if (xremotes == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < xremotes->len; i++) {
+ const gchar *remote_name;
+ guint64 tmp;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GFile) file_timestamp = NULL;
+ g_autofree gchar *appstream_fn = NULL;
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ /* not enabled */
+ if (flatpak_remote_get_disabled (xremote))
+ continue;
+
+ remote_name = flatpak_remote_get_name (xremote);
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+
+ /* skip known-broken repos */
+ if (g_hash_table_lookup (self->broken_remotes, remote_name) != NULL) {
+ g_debug ("skipping known broken remote: %s", remote_name);
+ continue;
+ }
+
+ g_clear_pointer (&locker, g_mutex_locker_free);
+
+ /* is the timestamp new enough */
+ file_timestamp = flatpak_remote_get_appstream_timestamp (xremote, NULL);
+ tmp = gs_utils_get_file_age (file_timestamp);
+ if (tmp < cache_age_secs) {
+ g_autofree gchar *fn = g_file_get_path (file_timestamp);
+ g_debug ("%s is only %" G_GUINT64_FORMAT " seconds old, so ignoring refresh",
+ fn, tmp);
+ continue;
+ }
+
+ /* download new data */
+ g_debug ("%s is %" G_GUINT64_FORMAT " seconds old, so downloading new data",
+ remote_name, tmp);
+ ret = gs_flatpak_refresh_appstream_remote (self,
+ remote_name,
+ interactive,
+ cancellable,
+ &error_local);
+ if (!ret) {
+ g_autoptr(GsPluginEvent) event = NULL;
+ if (g_error_matches (error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED)) {
+ g_debug ("Failed to get AppStream metadata: %s",
+ error_local->message);
+
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+
+ /* don't try to fetch this again until refresh() */
+ g_hash_table_insert (self->broken_remotes,
+ g_strdup (remote_name),
+ GUINT_TO_POINTER (1));
+ continue;
+ }
+
+ /* allow the plugin loader to decide if this should be
+ * shown the user, possibly only for interactive jobs */
+ gs_flatpak_error_convert (&error_local);
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (self->plugin, event);
+ continue;
+ }
+
+ /* add the new AppStream repo to the shared silo */
+ file = flatpak_remote_get_appstream_dir (xremote, NULL);
+ appstream_fn = g_file_get_path (file);
+ g_debug ("using AppStream metadata found at: %s", appstream_fn);
+ }
+
+ /* ensure the AppStream silo is up to date */
+ if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) {
+ gs_flatpak_internal_data_changed (self);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+gs_flatpak_set_metadata_installed (GsFlatpak *self,
+ GsApp *app,
+ FlatpakInstalledRef *xref,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ const gchar *appdata_version;
+ guint64 mtime;
+ guint64 size_installed;
+ g_autofree gchar *metadata_fn = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GFileInfo) info = NULL;
+
+ /* for all types */
+ gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref));
+ if (gs_app_get_metadata_item (app, "GnomeSoftware::Creator") == NULL) {
+ gs_app_set_metadata (app, "GnomeSoftware::Creator",
+ gs_plugin_get_name (self->plugin));
+ }
+
+ /* get the last time the app was updated */
+ metadata_fn = g_build_filename (flatpak_installed_ref_get_deploy_dir (xref),
+ "..",
+ "active",
+ "metadata",
+ NULL);
+ file = g_file_new_for_path (metadata_fn);
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL, NULL);
+ if (info != NULL) {
+ mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ gs_app_set_install_date (app, mtime);
+ }
+
+ /* If it's a runtime, check if the main-app info should be set. Note that
+ * checking the app for AS_COMPONENT_KIND_RUNTIME is not good enough because it
+ * could be e.g. AS_COMPONENT_KIND_LOCALIZATION and still be a runtime from
+ * Flatpak's perspective.
+ */
+ if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME &&
+ gs_flatpak_app_get_main_app_ref_name (app) == NULL) {
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GKeyFile) metadata_file = NULL;
+ metadata_file = g_key_file_new ();
+ if (g_key_file_load_from_file (metadata_file, metadata_fn,
+ G_KEY_FILE_NONE, &error)) {
+ g_autofree gchar *main_app = g_key_file_get_string (metadata_file,
+ "ExtensionOf",
+ "ref", NULL);
+ if (main_app != NULL)
+ gs_flatpak_app_set_main_app_ref_name (app, main_app);
+ } else {
+ g_warning ("Error loading the metadata file for '%s': %s",
+ gs_app_get_unique_id (app), error->message);
+ }
+ }
+
+ /* this is faster than resolving */
+ if (gs_app_get_origin (app) == NULL)
+ gs_flatpak_set_app_origin (self, app, flatpak_installed_ref_get_origin (xref), NULL, interactive, cancellable);
+
+ /* this is faster than flatpak_installation_fetch_remote_size_sync() */
+ size_installed = flatpak_installed_ref_get_installed_size (xref);
+ gs_app_set_size_installed (app, (size_installed != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, size_installed);
+
+ appdata_version = flatpak_installed_ref_get_appdata_version (xref);
+ if (appdata_version != NULL)
+ gs_app_set_version (app, appdata_version);
+}
+
+static GsApp *
+gs_flatpak_create_installed (GsFlatpak *self,
+ FlatpakInstalledRef *xref,
+ FlatpakRemote *xremote,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autoptr(GsApp) app = NULL;
+ const gchar *origin;
+
+ g_return_val_if_fail (xref != NULL, NULL);
+
+ /* create new object */
+ origin = flatpak_installed_ref_get_origin (xref);
+ app = gs_flatpak_create_app (self, origin, FLATPAK_REF (xref), xremote, interactive, cancellable);
+
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+ gs_flatpak_set_metadata_installed (self, app, xref, interactive, cancellable);
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_flatpak_add_installed (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+
+ /* get apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs (gs_flatpak_get_installation (self, interactive),
+ cancellable, error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+ for (guint i = 0; i < xrefs->len; i++) {
+ FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i);
+ g_autoptr(GsApp) app = gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_sources (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+ g_autoptr(GPtrArray) xremotes = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* refresh */
+ if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+ return FALSE;
+
+ /* get installed apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs (installation,
+ cancellable,
+ error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* get available remotes */
+ xremotes = flatpak_installation_list_remotes (installation,
+ cancellable,
+ error);
+ if (xremotes == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < xremotes->len; i++) {
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ g_autoptr(GsApp) app = NULL;
+
+ /* apps installed from bundles add their own remote that only
+ * can be used for updating that app only -- so hide them */
+ if (flatpak_remote_get_noenumerate (xremote))
+ continue;
+
+ /* create app */
+ app = gs_flatpak_create_source (self, xremote);
+ gs_app_list_add (list, app);
+
+ /* add related apps, i.e. what was installed from there */
+ for (guint j = 0; j < xrefs->len; j++) {
+ FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, j);
+ g_autoptr(GsApp) related = NULL;
+
+ /* only apps */
+ if (flatpak_ref_get_kind (FLATPAK_REF (xref)) != FLATPAK_REF_KIND_APP)
+ continue;
+ if (g_strcmp0 (flatpak_installed_ref_get_origin (xref),
+ flatpak_remote_get_name (xremote)) != 0)
+ continue;
+ related = gs_flatpak_create_installed (self, xref, xremote, interactive, cancellable);
+ gs_app_add_related (app, related);
+ }
+ }
+ return TRUE;
+}
+
+GsApp *
+gs_flatpak_find_source_by_url (GsFlatpak *self,
+ const gchar *url,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ g_return_val_if_fail (url != NULL, NULL);
+
+ xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, error);
+ if (xremotes == NULL)
+ return NULL;
+ for (guint i = 0; i < xremotes->len; i++) {
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ g_autofree gchar *url_tmp = flatpak_remote_get_url (xremote);
+ if (g_strcmp0 (url, url_tmp) == 0)
+ return gs_flatpak_create_source (self, xremote);
+ }
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "cannot find %s", url);
+ return NULL;
+}
+
+/* transfer full */
+GsApp *
+gs_flatpak_ref_to_app (GsFlatpak *self,
+ const gchar *ref,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xremotes = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ g_return_val_if_fail (ref != NULL, NULL);
+
+ g_mutex_lock (&self->installed_refs_mutex);
+
+ if (self->installed_refs == NULL) {
+ self->installed_refs = flatpak_installation_list_installed_refs (installation,
+ cancellable, error);
+
+ if (self->installed_refs == NULL) {
+ g_mutex_unlock (&self->installed_refs_mutex);
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ }
+
+ for (guint i = 0; i < self->installed_refs->len; i++) {
+ g_autoptr(FlatpakInstalledRef) xref = g_object_ref (g_ptr_array_index (self->installed_refs, i));
+ g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref));
+ if (g_strcmp0 (ref, ref_tmp) == 0) {
+ g_mutex_unlock (&self->installed_refs_mutex);
+ return gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable);
+ }
+ }
+
+ g_mutex_unlock (&self->installed_refs_mutex);
+
+ /* look at each remote xref */
+ xremotes = flatpak_installation_list_remotes (installation,
+ cancellable, error);
+ if (xremotes == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ for (guint i = 0; i < xremotes->len; i++) {
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) refs_remote = NULL;
+
+ /* disabled */
+ if (flatpak_remote_get_disabled (xremote))
+ continue;
+ refs_remote = flatpak_installation_list_remote_refs_sync (installation,
+ flatpak_remote_get_name (xremote),
+ cancellable,
+ &error_local);
+ if (refs_remote == NULL) {
+ g_debug ("failed to list refs in '%s': %s",
+ flatpak_remote_get_name (xremote),
+ error_local->message);
+ continue;
+ }
+ for (guint j = 0; j < refs_remote->len; j++) {
+ FlatpakRef *xref = g_ptr_array_index (refs_remote, j);
+ g_autofree gchar *ref_tmp = flatpak_ref_format_ref (xref);
+ if (g_strcmp0 (ref, ref_tmp) == 0) {
+ const gchar *origin = flatpak_remote_get_name (xremote);
+ return gs_flatpak_create_app (self, origin, xref, xremote, interactive, cancellable);
+ }
+ }
+ }
+
+ /* nothing found */
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "cannot find %s", ref);
+ return NULL;
+}
+
+/* This is essentially the inverse of gs_flatpak_app_new_from_repo_file() */
+static void
+gs_flatpak_update_remote_from_app (GsFlatpak *self,
+ FlatpakRemote *xremote,
+ GsApp *app)
+{
+ const gchar *gpg_key;
+ const gchar *branch;
+ const gchar *title, *homepage, *comment, *description;
+ const gchar *filter;
+ GPtrArray *icons;
+
+ flatpak_remote_set_disabled (xremote, FALSE);
+
+ flatpak_remote_set_url (xremote, gs_flatpak_app_get_repo_url (app));
+ flatpak_remote_set_noenumerate (xremote, FALSE);
+
+ title = gs_app_get_name (app);
+ if (title != NULL)
+ flatpak_remote_set_title (xremote, title);
+
+ /* decode GPG key if set */
+ gpg_key = gs_flatpak_app_get_repo_gpgkey (app);
+ if (gpg_key != NULL) {
+ gsize data_len = 0;
+ g_autofree guchar *data = NULL;
+ g_autoptr(GBytes) bytes = NULL;
+ data = g_base64_decode (gpg_key, &data_len);
+ bytes = g_bytes_new (data, data_len);
+ flatpak_remote_set_gpg_verify (xremote, TRUE);
+ flatpak_remote_set_gpg_key (xremote, bytes);
+ } else {
+ flatpak_remote_set_gpg_verify (xremote, FALSE);
+ }
+
+ /* default branch */
+ branch = gs_app_get_branch (app);
+ if (branch != NULL)
+ flatpak_remote_set_default_branch (xremote, branch);
+
+ /* optional data */
+ homepage = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE);
+ if (homepage != NULL)
+ flatpak_remote_set_homepage (xremote, homepage);
+
+ comment = gs_app_get_summary (app);
+ if (comment != NULL)
+ flatpak_remote_set_comment (xremote, comment);
+
+ description = gs_app_get_description (app);
+ if (description != NULL)
+ flatpak_remote_set_description (xremote, description);
+
+ icons = gs_app_get_icons (app);
+ for (guint i = 0; icons != NULL && i < icons->len; i++) {
+ GIcon *icon = g_ptr_array_index (icons, i);
+
+ if (GS_IS_REMOTE_ICON (icon)) {
+ flatpak_remote_set_icon (xremote,
+ gs_remote_icon_get_uri (GS_REMOTE_ICON (icon)));
+ break;
+ }
+ }
+
+ /* With the other fields, we always want to add as much information as
+ * we can to the @xremote. With the filter, though, we want to drop it
+ * if no filter is set on the @app. Importing an updated flatpakrepo
+ * file is one of the methods for switching from (for example) filtered
+ * flathub to unfiltered flathub. So if @app doesn’t have a filter set,
+ * clear it on the @xremote (i.e. don’t check for NULL). */
+ filter = gs_flatpak_app_get_repo_filter (app);
+ flatpak_remote_set_filter (xremote, filter);
+}
+
+static FlatpakRemote *
+gs_flatpak_create_new_remote (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+
+ /* create a new remote */
+ xremote = flatpak_remote_new (gs_app_get_id (app));
+ gs_flatpak_update_remote_from_app (self, xremote, app);
+
+ return g_steal_pointer (&xremote);
+}
+
+/* @is_install is %TRUE if the repo is being installed, or %FALSE if it’s being
+ * enabled. If it’s being enabled, no properties apart from enabled/disabled
+ * should be modified. */
+gboolean
+gs_flatpak_app_install_source (GsFlatpak *self,
+ GsApp *app,
+ gboolean is_install,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ xremote = flatpak_installation_get_remote_by_name (installation,
+ gs_app_get_id (app),
+ cancellable, NULL);
+ if (xremote != NULL) {
+ /* if the remote already exists, just enable it and update it */
+ g_debug ("modifying existing remote %s", flatpak_remote_get_name (xremote));
+ flatpak_remote_set_disabled (xremote, FALSE);
+
+ if (is_install &&
+ gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REPO) {
+ gs_flatpak_update_remote_from_app (self, xremote, app);
+ }
+ } else if (!is_install) {
+ g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, "Cannot enable flatpak remote '%s', remote not found", gs_app_get_id (app));
+ } else {
+ /* create a new remote */
+ xremote = gs_flatpak_create_new_remote (self, app, cancellable, error);
+ }
+
+ /* install it */
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ if (!flatpak_installation_modify_remote (installation,
+ xremote,
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ g_prefix_error (error, "cannot modify remote: ");
+ gs_app_set_state_recover (app);
+ gs_flatpak_internal_data_changed (self);
+ return FALSE;
+ }
+
+ /* Mark the internal cache as obsolete. */
+ gs_flatpak_internal_data_changed (self);
+
+ /* success */
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+ gs_plugin_repository_changed (self->plugin, app);
+
+ return TRUE;
+}
+
+static GsApp *
+get_main_app_of_related (GsFlatpak *self,
+ GsApp *related_app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakInstalledRef) ref = NULL;
+ const gchar *ref_name;
+ g_auto(GStrv) app_tokens = NULL;
+ FlatpakRefKind ref_kind = FLATPAK_REF_KIND_RUNTIME;
+
+ ref_name = gs_flatpak_app_get_main_app_ref_name (related_app);
+ if (ref_name == NULL) {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+ "%s doesn't have a main app set to it.",
+ gs_app_get_unique_id (related_app));
+ return NULL;
+ }
+
+ app_tokens = g_strsplit (ref_name, "/", -1);
+ if (g_strv_length (app_tokens) != 4) {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ "The main app of %s has an invalid name: %s",
+ gs_app_get_unique_id (related_app), ref_name);
+ return NULL;
+ }
+
+ /* get the right ref kind for the main app */
+ if (g_strcmp0 (app_tokens[0], "app") == 0)
+ ref_kind = FLATPAK_REF_KIND_APP;
+
+ /* this function only returns G_IO_ERROR_NOT_FOUND when the metadata file
+ * is missing, but if that's the case then things should have broken before
+ * this point */
+ ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive),
+ ref_kind,
+ app_tokens[1],
+ app_tokens[2],
+ app_tokens[3],
+ cancellable,
+ error);
+ if (ref == NULL)
+ return NULL;
+
+ return gs_flatpak_create_installed (self, ref, NULL, interactive, cancellable);
+}
+
+static GsApp *
+get_real_app_for_update (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsApp *main_app = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME)
+ main_app = get_main_app_of_related (self, app, interactive, cancellable, &error_local);
+
+ if (main_app == NULL) {
+ /* not all runtimes are extensions, and in that case we get the
+ * not-found error, so we only report other types of errors */
+ if (error_local != NULL &&
+ !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+
+ main_app = g_object_ref (app);
+ } else {
+ g_debug ("Related extension app %s of main app %s is updatable, so "
+ "setting the latter's state instead.", gs_app_get_unique_id (app),
+ gs_app_get_unique_id (main_app));
+ gs_app_set_state (main_app, GS_APP_STATE_UPDATABLE_LIVE);
+ /* Make sure the 'app' is not forgotten, it'll be added into the transaction later */
+ gs_app_add_related (main_app, app);
+ }
+
+ return main_app;
+}
+
+gboolean
+gs_flatpak_add_updates (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+ return FALSE;
+
+ /* get all the updatable apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs_for_update (installation,
+ cancellable,
+ error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+ /* look at each installed xref */
+ for (guint i = 0; i < xrefs->len; i++) {
+ FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i);
+ const gchar *commit;
+ const gchar *latest_commit;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsApp) main_app = NULL;
+
+ /* check the application has already been downloaded */
+ commit = flatpak_ref_get_commit (FLATPAK_REF (xref));
+ latest_commit = flatpak_installed_ref_get_latest_commit (xref);
+ app = gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable);
+ main_app = get_real_app_for_update (self, app, interactive, cancellable, &error_local);
+ if (main_app == NULL) {
+ g_debug ("Couldn't get the main app for updatable app extension %s: "
+ "%s; adding the app itself to the updates list...",
+ gs_app_get_unique_id (app), error_local->message);
+ g_clear_error (&error_local);
+ main_app = g_object_ref (app);
+ }
+
+ /* if for some reason the app is already getting updated, then
+ * don't change its state */
+ if (gs_app_get_state (main_app) != GS_APP_STATE_INSTALLING)
+ gs_app_set_state (main_app, GS_APP_STATE_UPDATABLE_LIVE);
+
+ /* set updatable state on the extension too, as it will have
+ * its state updated to installing then installed later on */
+ if (gs_app_get_state (app) != GS_APP_STATE_INSTALLING)
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+
+ /* already downloaded */
+ if (latest_commit && g_strcmp0 (commit, latest_commit) != 0) {
+ g_debug ("%s has a downloaded update %s->%s",
+ flatpak_ref_get_name (FLATPAK_REF (xref)),
+ commit, latest_commit);
+ gs_app_set_update_details_markup (main_app, NULL);
+ gs_app_set_update_version (main_app, NULL);
+ gs_app_set_update_urgency (main_app, AS_URGENCY_KIND_UNKNOWN);
+ gs_app_set_size_download (main_app, GS_SIZE_TYPE_VALID, 0);
+
+ /* needs download */
+ } else {
+ guint64 download_size = 0;
+ g_debug ("%s needs update",
+ flatpak_ref_get_name (FLATPAK_REF (xref)));
+
+ /* get the current download size */
+ if (gs_app_get_size_download (main_app, NULL) != GS_SIZE_TYPE_VALID) {
+ if (!flatpak_installation_fetch_remote_size_sync (installation,
+ gs_app_get_origin (app),
+ FLATPAK_REF (xref),
+ &download_size,
+ NULL,
+ cancellable,
+ &error_local)) {
+ g_warning ("failed to get download size: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ gs_app_set_size_download (main_app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ } else {
+ gs_app_set_size_download (main_app, GS_SIZE_TYPE_VALID, download_size);
+ }
+ }
+ }
+ gs_flatpak_set_update_permissions (self, main_app, xref, interactive, cancellable);
+ gs_app_list_add (list, main_app);
+ }
+
+ /* success */
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_refresh (GsFlatpak *self,
+ guint64 cache_age_secs,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* give all the repos a second chance */
+ g_mutex_lock (&self->broken_remotes_mutex);
+ g_hash_table_remove_all (self->broken_remotes);
+ g_mutex_unlock (&self->broken_remotes_mutex);
+
+ /* manually drop the cache in both installation instances;
+ * it's needed to have them both agree on the content. */
+ if (!flatpak_installation_drop_caches (gs_flatpak_get_installation (self, FALSE),
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ if (!flatpak_installation_drop_caches (gs_flatpak_get_installation (self, TRUE),
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* drop the installed refs cache */
+ g_mutex_lock (&self->installed_refs_mutex);
+ g_clear_pointer (&self->installed_refs, g_ptr_array_unref);
+ g_mutex_unlock (&self->installed_refs_mutex);
+
+ /* manually do this in case we created the first appstream file */
+ gs_flatpak_invalidate_silo (self);
+
+ /* update AppStream metadata */
+ if (!gs_flatpak_refresh_appstream (self, cache_age_secs, interactive, cancellable, error))
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_refine_item_origin_hostname (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ g_autofree gchar *url = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* already set */
+ if (gs_app_get_origin_hostname (app) != NULL)
+ return TRUE;
+
+ /* no origin */
+ if (gs_app_get_origin (app) == NULL)
+ return TRUE;
+
+ /* get the remote */
+ xremote = flatpak_installation_get_remote_by_name (gs_flatpak_get_installation (self, interactive),
+ gs_app_get_origin (app),
+ cancellable,
+ &error_local);
+ if (xremote == NULL) {
+ if (g_error_matches (error_local,
+ FLATPAK_ERROR,
+ FLATPAK_ERROR_REMOTE_NOT_FOUND)) {
+ /* if the user deletes the -origin remote for a locally
+ * installed flatpakref file then we should just show
+ * 'localhost' and not return an error */
+ gs_app_set_origin_hostname (app, "");
+ return TRUE;
+ }
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ url = flatpak_remote_get_url (xremote);
+ if (url == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "no URL for remote %s",
+ flatpak_remote_get_name (xremote));
+ return FALSE;
+ }
+ gs_app_set_origin_hostname (app, url);
+ return TRUE;
+}
+
+static gboolean
+gs_refine_item_metadata (GsFlatpak *self,
+ GsApp *app,
+ GError **error)
+{
+ g_autoptr(FlatpakRef) xref = NULL;
+
+ /* already set */
+ if (gs_flatpak_app_get_ref_name (app) != NULL)
+ return TRUE;
+
+ /* not a valid type */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
+ return TRUE;
+
+ /* AppStream sets the source to appname/arch/branch, if this isn't set
+ * we can't break out the fields */
+ if (gs_app_get_source_default (app) == NULL) {
+ g_autofree gchar *tmp = gs_app_to_string (app);
+ g_warning ("no source set by appstream for %s: %s",
+ gs_plugin_get_name (self->plugin), tmp);
+ return TRUE;
+ }
+
+ /* parse the ref */
+ xref = flatpak_ref_parse (gs_app_get_source_default (app), error);
+ if (xref == NULL) {
+ gs_flatpak_error_convert (error);
+ g_prefix_error (error, "failed to parse '%s': ",
+ gs_app_get_source_default (app));
+ return FALSE;
+ }
+ gs_flatpak_set_metadata (self, app, xref);
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_refine_item_origin (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *ref_display = NULL;
+ g_autoptr(GPtrArray) xremotes = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* already set */
+ if (gs_app_get_origin (app) != NULL)
+ return TRUE;
+
+ /* not applicable */
+ if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE_LOCAL)
+ return TRUE;
+
+ /* ensure metadata exists */
+ if (!gs_refine_item_metadata (self, app, error))
+ return FALSE;
+
+ /* find list of remotes */
+ ref_display = gs_flatpak_app_get_ref_display (app);
+ g_debug ("looking for a remote for %s", ref_display);
+ xremotes = flatpak_installation_list_remotes (installation,
+ cancellable, error);
+ if (xremotes == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < xremotes->len; i++) {
+ const gchar *remote_name;
+ FlatpakRemote *xremote = g_ptr_array_index (xremotes, i);
+ g_autoptr(FlatpakRemoteRef) xref = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* not enabled */
+ if (flatpak_remote_get_disabled (xremote))
+ continue;
+
+ /* sync */
+ remote_name = flatpak_remote_get_name (xremote);
+ g_debug ("looking at remote %s", remote_name);
+ xref = flatpak_installation_fetch_remote_ref_sync (installation,
+ remote_name,
+ gs_flatpak_app_get_ref_kind (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ cancellable,
+ &error_local);
+ if (xref != NULL) {
+ g_debug ("found remote %s", remote_name);
+ gs_flatpak_set_app_origin (self, app, remote_name, xremote, interactive, cancellable);
+ gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (FLATPAK_REF (xref)));
+ gs_plugin_refine_item_scope (self, app);
+ return TRUE;
+ }
+ g_debug ("%s failed to find remote %s: %s",
+ ref_display, remote_name, error_local->message);
+ }
+
+ /* not found */
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "%s not found in any remote",
+ ref_display);
+ return FALSE;
+}
+
+static FlatpakRef *
+gs_flatpak_create_fake_ref (GsApp *app, GError **error)
+{
+ FlatpakRef *xref;
+ g_autofree gchar *id = NULL;
+ id = g_strdup_printf ("%s/%s/%s/%s",
+ gs_flatpak_app_get_ref_kind_as_str (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app));
+ xref = flatpak_ref_parse (id, error);
+ if (xref == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ return xref;
+}
+
+/* the _unlocked() version doesn't call gs_flatpak_rescan_app_data,
+ * in order to avoid taking the writer lock on self->silo_lock */
+static gboolean
+gs_flatpak_refine_app_state_unlocked (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakInstalledRef) ref = NULL;
+ g_autoptr(GPtrArray) installed_refs = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* already found */
+ if (gs_app_get_state (app) != GS_APP_STATE_UNKNOWN)
+ return TRUE;
+
+ /* need broken out metadata */
+ if (!gs_refine_item_metadata (self, app, error))
+ return FALSE;
+
+ /* ensure origin set */
+ if (!gs_plugin_refine_item_origin (self, app, interactive, cancellable, error))
+ return FALSE;
+
+ /* find the app using the origin and the ID */
+ g_mutex_lock (&self->installed_refs_mutex);
+
+ if (self->installed_refs == NULL) {
+ self->installed_refs = flatpak_installation_list_installed_refs (installation,
+ cancellable, error);
+
+ if (self->installed_refs == NULL) {
+ g_mutex_unlock (&self->installed_refs_mutex);
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+
+ installed_refs = g_ptr_array_ref (self->installed_refs);
+
+ for (guint i = 0; i < installed_refs->len; i++) {
+ FlatpakInstalledRef *ref_tmp = g_ptr_array_index (installed_refs, i);
+ const gchar *origin = flatpak_installed_ref_get_origin (ref_tmp);
+ const gchar *name = flatpak_ref_get_name (FLATPAK_REF (ref_tmp));
+ const gchar *arch = flatpak_ref_get_arch (FLATPAK_REF (ref_tmp));
+ const gchar *branch = flatpak_ref_get_branch (FLATPAK_REF (ref_tmp));
+ if (g_strcmp0 (origin, gs_app_get_origin (app)) == 0 &&
+ g_strcmp0 (name, gs_flatpak_app_get_ref_name (app)) == 0 &&
+ g_strcmp0 (arch, gs_flatpak_app_get_ref_arch (app)) == 0 &&
+ g_strcmp0 (branch, gs_app_get_branch (app)) == 0) {
+ ref = g_object_ref (ref_tmp);
+ break;
+ }
+ }
+ g_mutex_unlock (&self->installed_refs_mutex);
+ if (ref != NULL) {
+ g_debug ("marking %s as installed with flatpak",
+ gs_app_get_unique_id (app));
+ gs_flatpak_set_metadata_installed (self, app, ref, interactive, cancellable);
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+ /* flatpak only allows one installed app to be launchable */
+ if (flatpak_installed_ref_get_is_current (ref)) {
+ gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ } else {
+ g_debug ("%s is not current, and therefore not launchable",
+ gs_app_get_unique_id (app));
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ }
+ return TRUE;
+ }
+
+ /* anything not installed just check the remote is still present */
+ if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN &&
+ gs_app_get_origin (app) != NULL) {
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ xremote = flatpak_installation_get_remote_by_name (installation,
+ gs_app_get_origin (app),
+ cancellable, NULL);
+ if (xremote != NULL) {
+ if (flatpak_remote_get_disabled (xremote)) {
+ g_debug ("%s is available with flatpak "
+ "but %s is disabled",
+ gs_app_get_unique_id (app),
+ flatpak_remote_get_name (xremote));
+ gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
+ } else {
+ g_debug ("marking %s as available with flatpak",
+ gs_app_get_unique_id (app));
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+ }
+ } else {
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ g_debug ("failed to find %s remote %s for %s",
+ self->id,
+ gs_app_get_origin (app),
+ gs_app_get_unique_id (app));
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_refine_app_state (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* ensure valid */
+ if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+ return FALSE;
+
+ return gs_flatpak_refine_app_state_unlocked (self, app, interactive, cancellable, error);
+}
+
+static GsApp *
+gs_flatpak_create_runtime (GsFlatpak *self,
+ GsApp *parent,
+ const gchar *runtime,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autofree gchar *source = NULL;
+ g_auto(GStrv) split = NULL;
+ g_autoptr(GsApp) app_cache = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GError) local_error = NULL;
+ const gchar *origin;
+
+ /* get the name/arch/branch */
+ split = g_strsplit (runtime, "/", -1);
+ if (g_strv_length (split) != 3)
+ return NULL;
+
+ /* create the complete GsApp from the single string */
+ app = gs_app_new (split[0]);
+ gs_flatpak_claim_app (self, app);
+ source = g_strdup_printf ("runtime/%s", runtime);
+ gs_app_add_source (app, source);
+ gs_app_set_kind (app, AS_COMPONENT_KIND_RUNTIME);
+ gs_app_set_branch (app, split[2]);
+
+ origin = gs_app_get_origin (parent);
+ if (origin != NULL) {
+ g_autoptr(FlatpakRemoteRef) xref = NULL;
+
+ xref = flatpak_installation_fetch_remote_ref_sync (gs_flatpak_get_installation (self, interactive),
+ origin,
+ FLATPAK_REF_KIND_RUNTIME,
+ gs_app_get_id (app),
+ gs_flatpak_app_get_ref_arch (parent),
+ gs_app_get_branch (app),
+ cancellable,
+ NULL);
+
+ /* Prefer runtime from the same origin as the parent application */
+ if (xref)
+ gs_app_set_origin (app, origin);
+ }
+
+ /* search in the cache */
+ app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cache != NULL &&
+ g_strcmp0 (gs_flatpak_app_get_ref_name (app_cache), split[0]) == 0 &&
+ g_strcmp0 (gs_flatpak_app_get_ref_arch (app_cache), split[1]) == 0 &&
+ g_strcmp0 (gs_app_get_branch (app_cache), split[2]) == 0) {
+ /* since the cached runtime can have been created somewhere else
+ * (we're using a global cache), we need to make sure that a
+ * source is set */
+ if (gs_app_get_source_default (app_cache) == NULL)
+ gs_app_add_source (app_cache, source);
+ return g_steal_pointer (&app_cache);
+ } else {
+ g_clear_object (&app_cache);
+ }
+
+ /* if the app is per-user we can also use the installed system runtime */
+ if (gs_app_get_scope (parent) == AS_COMPONENT_SCOPE_USER) {
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN);
+ app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cache != NULL &&
+ g_strcmp0 (gs_flatpak_app_get_ref_name (app_cache), split[0]) == 0 &&
+ g_strcmp0 (gs_flatpak_app_get_ref_arch (app_cache), split[1]) == 0 &&
+ g_strcmp0 (gs_app_get_branch (app_cache), split[2]) == 0) {
+ return g_steal_pointer (&app_cache);
+ } else {
+ g_clear_object (&app_cache);
+ }
+ }
+
+ /* set superclassed app properties */
+ gs_flatpak_app_set_ref_kind (app, FLATPAK_REF_KIND_RUNTIME);
+ gs_flatpak_app_set_ref_name (app, split[0]);
+ gs_flatpak_app_set_ref_arch (app, split[1]);
+
+ if (!gs_flatpak_refine_app_state_unlocked (self, app, interactive, NULL, &local_error))
+ g_debug ("Failed to refine state for runtime '%s': %s", gs_app_get_unique_id (app), local_error->message);
+
+ /* save in the cache */
+ gs_plugin_cache_add (self->plugin, NULL, app);
+ return g_steal_pointer (&app);
+}
+
+static gboolean
+gs_flatpak_set_app_metadata (GsFlatpak *self,
+ GsApp *app,
+ const gchar *data,
+ gsize length,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean secure = TRUE;
+ g_autofree gchar *name = NULL;
+ g_autofree gchar *runtime = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(GsApp) app_runtime = NULL;
+ g_autoptr(GsAppPermissions) permissions = NULL;
+ g_auto(GStrv) shared = NULL;
+ g_auto(GStrv) sockets = NULL;
+ g_auto(GStrv) filesystems = NULL;
+
+ kf = g_key_file_new ();
+ if (!g_key_file_load_from_data (kf, data, length, G_KEY_FILE_NONE, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ name = g_key_file_get_string (kf, "Application", "name", error);
+ if (name == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ gs_flatpak_app_set_ref_name (app, name);
+ runtime = g_key_file_get_string (kf, "Application", "runtime", error);
+ if (runtime == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ shared = g_key_file_get_string_list (kf, "Context", "shared", NULL, NULL);
+ if (shared != NULL) {
+ /* SHM isn't secure enough */
+ if (g_strv_contains ((const gchar * const *) shared, "ipc"))
+ secure = FALSE;
+ }
+ sockets = g_key_file_get_string_list (kf, "Context", "sockets", NULL, NULL);
+ if (sockets != NULL) {
+ /* X11 isn't secure enough */
+ if (g_strv_contains ((const gchar * const *) sockets, "x11"))
+ secure = FALSE;
+ }
+ filesystems = g_key_file_get_string_list (kf, "Context", "filesystems", NULL, NULL);
+ if (filesystems != NULL) {
+ /* secure apps should be using portals */
+ if (g_strv_contains ((const gchar * const *) filesystems, "home"))
+ secure = FALSE;
+ }
+
+ permissions = perms_from_metadata (kf);
+ gs_app_set_permissions (app, permissions);
+ /* this is actually quite hard to achieve */
+ if (secure)
+ gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED_SECURE);
+
+ /* create runtime */
+ app_runtime = gs_flatpak_create_runtime (self, app, runtime, interactive, cancellable);
+ if (app_runtime != NULL) {
+ gs_plugin_refine_item_scope (self, app_runtime);
+ gs_app_set_runtime (app, app_runtime);
+ }
+
+ /* we always get this, but it's a low bar... */
+ gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED);
+
+ return TRUE;
+}
+
+static GBytes *
+gs_flatpak_fetch_remote_metadata (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GBytes) data = NULL;
+ g_autoptr(FlatpakRef) xref = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ /* no origin */
+ if (gs_app_get_origin (app) == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no origin set when getting metadata for %s",
+ gs_app_get_unique_id (app));
+ return NULL;
+ }
+
+ /* fetch from the server */
+ xref = gs_flatpak_create_fake_ref (app, error);
+ if (xref == NULL)
+ return NULL;
+ data = flatpak_installation_fetch_remote_metadata_sync (gs_flatpak_get_installation (self, interactive),
+ gs_app_get_origin (app),
+ xref,
+ cancellable,
+ &local_error);
+ if (data == NULL) {
+ if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_REF_NOT_FOUND) &&
+ !gs_plugin_get_network_available (self->plugin)) {
+ local_error->code = GS_PLUGIN_ERROR_NO_NETWORK;
+ local_error->domain = GS_PLUGIN_ERROR;
+ } else {
+ gs_flatpak_error_convert (&local_error);
+ }
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return NULL;
+ }
+ return g_steal_pointer (&data);
+}
+
+static gboolean
+gs_plugin_refine_item_metadata (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *str;
+ gsize len = 0;
+ g_autofree gchar *contents = NULL;
+ g_autofree gchar *installation_path_str = NULL;
+ g_autofree gchar *install_path = NULL;
+ g_autoptr(GBytes) data = NULL;
+ g_autoptr(GFile) installation_path = NULL;
+
+ /* not applicable */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
+ return TRUE;
+ if (gs_flatpak_app_get_ref_kind (app) != FLATPAK_REF_KIND_APP)
+ return TRUE;
+
+ /* already done */
+ if (gs_app_has_kudo (app, GS_APP_KUDO_SANDBOXED))
+ return TRUE;
+
+ /* this is quicker than doing network IO */
+ installation_path = flatpak_installation_get_path (self->installation_noninteractive);
+ installation_path_str = g_file_get_path (installation_path);
+ install_path = g_build_filename (installation_path_str,
+ gs_flatpak_app_get_ref_kind_as_str (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ "active",
+ "metadata",
+ NULL);
+ if (g_file_test (install_path, G_FILE_TEST_EXISTS)) {
+ if (!g_file_get_contents (install_path, &contents, &len, error))
+ return FALSE;
+ str = contents;
+ } else {
+ data = gs_flatpak_fetch_remote_metadata (self, app, interactive,
+ cancellable,
+ error);
+ if (data == NULL)
+ return FALSE;
+ str = g_bytes_get_data (data, &len);
+ }
+
+ /* parse key file */
+ if (!gs_flatpak_set_app_metadata (self, app, str, len, interactive, cancellable, error))
+ return FALSE;
+ return TRUE;
+}
+
+static FlatpakInstalledRef *
+gs_flatpak_get_installed_ref (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ FlatpakInstalledRef *ref;
+ ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive),
+ gs_flatpak_app_get_ref_kind (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ cancellable,
+ error);
+ if (ref == NULL)
+ gs_flatpak_error_convert (error);
+ return ref;
+}
+
+static gboolean
+gs_flatpak_prune_addons_list (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) addons_list = NULL;
+ g_autoptr(GPtrArray) installed_related_refs = NULL;
+ g_autoptr(GPtrArray) remote_related_refs = NULL;
+ g_autofree gchar *ref = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+ g_autoptr(GError) error_local = NULL;
+
+ addons_list = gs_app_dup_addons (app);
+ if (addons_list == NULL || gs_app_list_length (addons_list) == 0)
+ return TRUE;
+
+ if (gs_app_get_origin (app) == NULL)
+ return TRUE;
+
+ /* return early if the addons haven't been refined */
+ for (guint i = 0; i < gs_app_list_length (addons_list); i++) {
+ GsApp *app_addon = gs_app_list_index (addons_list, i);
+
+ if (gs_flatpak_app_get_ref_name (app_addon) == NULL ||
+ gs_flatpak_app_get_ref_arch (app_addon) == NULL ||
+ gs_app_get_branch (app_addon) == NULL)
+ return TRUE;
+ }
+
+ /* return early if the API we need isn't available */
+#if !FLATPAK_CHECK_VERSION(1,11,1)
+ if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
+ return TRUE;
+#endif
+
+ ref = g_strdup_printf ("%s/%s/%s/%s",
+ gs_flatpak_app_get_ref_kind_as_str (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app));
+
+ /* Find installed related refs in case the app is installed */
+ installed_related_refs = flatpak_installation_list_installed_related_refs_sync (installation,
+ gs_app_get_origin (app),
+ ref,
+ cancellable,
+ &error_local);
+ if (installed_related_refs == NULL &&
+ !g_error_matches (error_local,
+ FLATPAK_ERROR,
+ FLATPAK_ERROR_NOT_INSTALLED)) {
+ gs_flatpak_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ g_clear_error (&error_local);
+
+#if FLATPAK_CHECK_VERSION(1,11,1)
+ /* Find remote related refs that match the installed version in case the app is installed */
+ remote_related_refs = flatpak_installation_list_remote_related_refs_for_installed_sync (installation,
+ gs_app_get_origin (app),
+ ref,
+ cancellable,
+ &error_local);
+ if (remote_related_refs == NULL &&
+ !g_error_matches (error_local,
+ FLATPAK_ERROR,
+ FLATPAK_ERROR_NOT_INSTALLED)) {
+ gs_flatpak_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ g_clear_error (&error_local);
+#endif
+
+ /* Find remote related refs in case the app is not installed */
+ if (remote_related_refs == NULL) {
+ remote_related_refs = flatpak_installation_list_remote_related_refs_sync (installation,
+ gs_app_get_origin (app),
+ ref,
+ cancellable,
+ &error_local);
+ /* don't make the error fatal in case we're offline */
+ if (error_local != NULL)
+ g_debug ("failed to list remote related refs of %s: %s",
+ gs_app_get_unique_id (app), error_local->message);
+ }
+
+ g_clear_error (&error_local);
+
+ /* For each addon, if it is neither installed nor available, hide it
+ * since it may be intended for a different version of the app. We
+ * don't want to show both org.videolan.VLC.Plugin.bdj//3-19.08 and
+ * org.videolan.VLC.Plugin.bdj//3-20.08 in the UI; only one will work
+ * for the installed app
+ */
+ for (guint i = 0; i < gs_app_list_length (addons_list); i++) {
+ GsApp *app_addon = gs_app_list_index (addons_list, i);
+ gboolean found = FALSE;
+ g_autofree char *addon_ref = NULL;
+
+ addon_ref = g_strdup_printf ("%s/%s/%s/%s",
+ gs_flatpak_app_get_ref_kind_as_str (app_addon),
+ gs_flatpak_app_get_ref_name (app_addon),
+ gs_flatpak_app_get_ref_arch (app_addon),
+ gs_app_get_branch (app_addon));
+ for (guint j = 0; installed_related_refs && j < installed_related_refs->len; j++) {
+ FlatpakRelatedRef *rel = g_ptr_array_index (installed_related_refs, j);
+ g_autofree char *rel_ref = flatpak_ref_format_ref (FLATPAK_REF (rel));
+ if (g_strcmp0 (addon_ref, rel_ref) == 0)
+ found = TRUE;
+ }
+ for (guint j = 0; remote_related_refs && j < remote_related_refs->len; j++) {
+ FlatpakRelatedRef *rel = g_ptr_array_index (remote_related_refs, j);
+ g_autofree char *rel_ref = flatpak_ref_format_ref (FLATPAK_REF (rel));
+ if (g_strcmp0 (addon_ref, rel_ref) == 0)
+ found = TRUE;
+ }
+
+ if (!found) {
+ gs_app_add_quirk (app_addon, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ g_debug ("hiding %s since it's not related to %s",
+ addon_ref, gs_app_get_unique_id (app));
+ } else {
+ gs_app_remove_quirk (app_addon, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ g_debug ("unhiding %s since it's related to %s",
+ addon_ref, gs_app_get_unique_id (app));
+ }
+ }
+ return TRUE;
+}
+
+static guint64
+gs_flatpak_get_app_directory_size (GsApp *app,
+ const gchar *subdir_name,
+ GCancellable *cancellable)
+{
+ g_autofree gchar *filename = NULL;
+ filename = g_build_filename (g_get_home_dir (), ".var", "app", gs_app_get_id (app), subdir_name, NULL);
+ return gs_utils_get_file_size (filename, NULL, NULL, cancellable);
+}
+
+static gboolean
+gs_plugin_refine_item_size (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret;
+ guint64 download_size = 0;
+ guint64 installed_size = 0;
+ GsSizeType size_type = GS_SIZE_TYPE_UNKNOWABLE;
+
+ /* not applicable */
+ if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE_LOCAL)
+ return TRUE;
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
+ return TRUE;
+
+ /* already set */
+ if (gs_app_is_installed (app)) {
+ /* only care about the installed size if the app is installed */
+ if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_VALID)
+ return TRUE;
+ } else {
+ if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_VALID &&
+ gs_app_get_size_download (app, NULL) == GS_SIZE_TYPE_VALID)
+ return TRUE;
+ }
+
+ /* need runtime */
+ if (!gs_plugin_refine_item_metadata (self, app, interactive, cancellable, error))
+ return FALSE;
+
+ /* calculate the platform size too if the app is not installed */
+ if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE &&
+ gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_APP) {
+ GsApp *app_runtime;
+
+ /* is the app_runtime already installed? */
+ app_runtime = gs_app_get_runtime (app);
+ if (!gs_flatpak_refine_app_state_unlocked (self,
+ app_runtime,
+ interactive,
+ cancellable,
+ error))
+ return FALSE;
+ if (gs_app_get_state (app_runtime) == GS_APP_STATE_INSTALLED) {
+ g_debug ("runtime %s is already installed, so not adding size",
+ gs_app_get_unique_id (app_runtime));
+ } else {
+ if (!gs_plugin_refine_item_size (self,
+ app_runtime,
+ interactive,
+ cancellable,
+ error))
+ return FALSE;
+ }
+ }
+
+ /* just get the size of the app */
+ if (!gs_plugin_refine_item_origin (self, app, interactive,
+ cancellable, error))
+ return FALSE;
+
+ /* if the app is installed we use the ref to fetch the installed size
+ * and ignore the download size as this is faster */
+ if (gs_app_is_installed (app)) {
+ g_autoptr(FlatpakInstalledRef) xref = NULL;
+ xref = gs_flatpak_get_installed_ref (self, app, interactive, cancellable, error);
+ if (xref == NULL)
+ return FALSE;
+ installed_size = flatpak_installed_ref_get_installed_size (xref);
+ size_type = (installed_size > 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWABLE;
+ } else {
+ g_autoptr(FlatpakRef) xref = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* no origin */
+ if (gs_app_get_origin (app) == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no origin set for %s",
+ gs_app_get_unique_id (app));
+ return FALSE;
+ }
+ xref = gs_flatpak_create_fake_ref (app, error);
+ if (xref == NULL)
+ return FALSE;
+ ret = flatpak_installation_fetch_remote_size_sync (gs_flatpak_get_installation (self, interactive),
+ gs_app_get_origin (app),
+ xref,
+ &download_size,
+ &installed_size,
+ cancellable,
+ &error_local);
+
+ if (!ret) {
+ /* This can happen when the remote is filtered */
+ g_debug ("libflatpak failed to return application size: %s", error_local->message);
+ g_clear_error (&error_local);
+ } else {
+ size_type = GS_SIZE_TYPE_VALID;
+ }
+ }
+
+ gs_app_set_size_installed (app, size_type, installed_size);
+ gs_app_set_size_download (app, size_type, download_size);
+
+ return TRUE;
+}
+
+static void
+gs_flatpak_refine_appstream_release (XbNode *component, GsApp *app)
+{
+ const gchar *version;
+
+ /* get first release */
+ version = xb_node_query_attr (component, "releases/release", "version", NULL);
+ if (version == NULL)
+ return;
+ switch (gs_app_get_state (app)) {
+ case GS_APP_STATE_INSTALLED:
+ case GS_APP_STATE_AVAILABLE:
+ case GS_APP_STATE_AVAILABLE_LOCAL:
+ gs_app_set_version (app, version);
+ break;
+ case GS_APP_STATE_UPDATABLE:
+ case GS_APP_STATE_UPDATABLE_LIVE:
+ gs_app_set_update_version (app, version);
+ break;
+ default:
+ g_debug ("%s is not installed, so ignoring version of %s",
+ gs_app_get_unique_id (app), version);
+ break;
+ }
+}
+
+/* This function is like gs_flatpak_refine_appstream(), but takes gzip
+ * compressed appstream data as a GBytes and assumes they are already uniquely
+ * tied to the app (and therefore app ID alone can be used to find the right
+ * component).
+ */
+static gboolean
+gs_flatpak_refine_appstream_from_bytes (GsFlatpak *self,
+ GsApp *app,
+ const char *origin, /* (nullable) */
+ FlatpakInstalledRef *installed_ref, /* (nullable) */
+ GBytes *appstream_gz,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *const *locales = g_get_language_names ();
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(XbBuilder) builder = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
+ g_autoptr(XbNode) component_node = NULL;
+ g_autoptr(XbNode) n = NULL;
+ g_autoptr(XbSilo) silo = NULL;
+ g_autoptr(XbBuilderFixup) bundle_fixup = NULL;
+ g_autoptr(GBytes) appstream = NULL;
+ g_autoptr(GInputStream) stream_data = NULL;
+ g_autoptr(GInputStream) stream_gz = NULL;
+ g_autoptr(GZlibDecompressor) decompressor = NULL;
+ g_autoptr(GMainContext) old_thread_default = NULL;
+
+ /* 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);
+
+ /* add current locales */
+ for (guint i = 0; locales[i] != NULL; i++)
+ xb_builder_add_locale (builder, locales[i]);
+
+ /* decompress data */
+ decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);
+ stream_gz = g_memory_input_stream_new_from_bytes (appstream_gz);
+ if (stream_gz == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "unable to decompress appstream data");
+ return FALSE;
+ }
+ stream_data = g_converter_input_stream_new (stream_gz,
+ G_CONVERTER (decompressor));
+
+ appstream = g_input_stream_read_bytes (stream_data,
+ 0x100000, /* 1Mb */
+ cancellable,
+ error);
+ if (appstream == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* build silo */
+ if (!xb_builder_source_load_bytes (source, appstream,
+ XB_BUILDER_SOURCE_FLAG_NONE,
+ error))
+ return FALSE;
+
+ /* Appdata from flatpak_installed_ref_load_appdata() may be missing the
+ * <bundle> tag but for this function we know it's the right component.
+ */
+ bundle_fixup = xb_builder_fixup_new ("AddBundle",
+ gs_flatpak_add_bundle_tag_cb,
+ gs_flatpak_app_get_ref_display (app), g_free);
+ xb_builder_fixup_set_max_depth (bundle_fixup, 2);
+ xb_builder_source_add_fixup (source, bundle_fixup);
+
+ fixup_flatpak_appstream_xml (source, origin);
+
+ /* add metadata */
+ if (installed_ref != NULL) {
+ g_autoptr(XbBuilderNode) info = NULL;
+ g_autofree char *icon_prefix = NULL;
+
+ info = xb_builder_node_insert (NULL, "info", NULL);
+ xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL);
+ icon_prefix = g_build_filename (flatpak_installed_ref_get_deploy_dir (installed_ref),
+ "files", "share", "app-info", "icons", "flatpak", NULL);
+ xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL);
+ xb_builder_source_set_info (source, info);
+ }
+
+ xb_builder_import_source (builder, source);
+
+ /* 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);
+
+ silo = xb_builder_compile (builder,
+ XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+ cancellable,
+ error);
+
+ if (old_thread_default != NULL)
+ g_main_context_push_thread_default (old_thread_default);
+
+ if (silo == NULL)
+ return FALSE;
+ if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
+ g_autofree gchar *xml = NULL;
+ xml = xb_silo_export (silo,
+ XB_NODE_EXPORT_FLAG_FORMAT_INDENT |
+ XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE,
+ NULL);
+ g_debug ("showing AppStream data: %s", xml);
+ }
+
+ /* check for sanity */
+ n = xb_silo_query_first (silo, "components/component", NULL);
+ if (n == NULL) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no apps found in AppStream data");
+ return FALSE;
+ }
+
+ /* find app */
+ xpath = g_strdup_printf ("components/component/id[text()='%s']/..",
+ gs_flatpak_app_get_ref_name (app));
+ component_node = xb_silo_query_first (silo, xpath, NULL);
+ if (component_node == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "application %s not found",
+ gs_flatpak_app_get_ref_name (app));
+ return FALSE;
+ }
+
+ /* copy details from AppStream to app */
+ if (!gs_appstream_refine_app (self->plugin, app, silo, component_node, flags, error))
+ return FALSE;
+
+ if (gs_app_get_origin (app))
+ gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, cancellable);
+
+ /* use the default release as the version number */
+ gs_flatpak_refine_appstream_release (component_node, app);
+
+ /* save the silo so it can be used for searches */
+ {
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->app_silos_mutex);
+ g_hash_table_replace (self->app_silos,
+ gs_flatpak_app_get_ref_display (app),
+ g_steal_pointer (&silo));
+ }
+
+ return TRUE;
+}
+
+static XbNode *
+get_renamed_component (GsFlatpak *self,
+ GsApp *app,
+ XbSilo *silo,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *origin = gs_app_get_origin (app);
+ const gchar *renamed_to;
+#if LIBXMLB_CHECK_VERSION(0, 3, 0)
+ g_autoptr(XbQuery) query = NULL;
+ g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT ();
+#else
+ g_autofree gchar *xpath = NULL;
+ g_autofree gchar *source_safe = NULL;
+#endif
+ g_autoptr(FlatpakRemoteRef) remote_ref = NULL;
+ g_autoptr(XbNode) component = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ remote_ref = flatpak_installation_fetch_remote_ref_sync (installation,
+ origin,
+ gs_flatpak_app_get_ref_kind (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ cancellable, error);
+ if (remote_ref == NULL)
+ return NULL;
+
+ renamed_to = flatpak_remote_ref_get_eol_rebase (remote_ref);
+ if (renamed_to == NULL)
+ return NULL;
+
+#if LIBXMLB_CHECK_VERSION(0, 3, 0)
+ query = xb_silo_lookup_query (silo, "components[@origin=?]/component/bundle[@type='flatpak'][text()=?]/..");
+ xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 0, origin, NULL);
+ xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 1, renamed_to, NULL);
+ component = xb_silo_query_first_with_context (silo, query, &context, NULL);
+#else
+ source_safe = xb_string_escape (renamed_to);
+ xpath = g_strdup_printf ("components[@origin='%s']/component/bundle[@type='flatpak'][text()='%s']/..",
+ origin, source_safe);
+ component = xb_silo_query_first (silo, xpath, NULL);
+#endif
+
+ /* Get the previous name so it can be displayed in the UI */
+ if (component != NULL) {
+ g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+ const gchar *installed_name = NULL;
+
+ installed_ref = flatpak_installation_get_installed_ref (installation,
+ gs_flatpak_app_get_ref_kind (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ cancellable, error);
+ if (installed_ref != NULL)
+ installed_name = flatpak_installed_ref_get_appdata_name (installed_ref);
+ if (installed_name != NULL)
+ gs_app_set_renamed_from (app, installed_name);
+ }
+
+ return g_steal_pointer (&component);
+}
+
+/* Returns %TRUE if @error exists and is set to G_IO_ERROR_CANCELLED */
+static inline gboolean
+propagate_cancelled_error (GError **dest,
+ GError **error)
+{
+ g_assert (error != NULL);
+
+ if (*error && g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_propagate_error (dest, g_steal_pointer (error));
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gs_flatpak_refine_appstream (GsFlatpak *self,
+ GsApp *app,
+ XbSilo *silo,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *origin = gs_app_get_origin (app);
+ const gchar *source = gs_app_get_source_default (app);
+ g_autofree gchar *source_safe = NULL;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(XbNode) component = NULL;
+
+ if (origin == NULL || source == NULL)
+ return TRUE;
+
+ /* find using source and origin */
+ source_safe = xb_string_escape (source);
+ xpath = g_strdup_printf ("components[@origin='%s']/component/bundle[@type='flatpak'][text()='%s']/..",
+ origin, source_safe);
+ component = xb_silo_query_first (silo, xpath, &error_local);
+
+ if (propagate_cancelled_error (error, &error_local))
+ return FALSE;
+
+ /* Ensure the gs_flatpak_app_get_ref_*() metadata are set */
+ gs_refine_item_metadata (self, app, NULL);
+
+ /* If the app was renamed, use the appstream data from the new name;
+ * usually it will not exist under the old name */
+ if (component == NULL && gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_APP) {
+ g_autoptr(GError) renamed_component_error = NULL;
+
+ component = get_renamed_component (self, app, silo,
+ interactive,
+ cancellable,
+ &renamed_component_error);
+
+ if (propagate_cancelled_error (error, &renamed_component_error))
+ return FALSE;
+ }
+
+ if (component == NULL) {
+ g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+ g_autoptr(GBytes) appstream_gz = NULL;
+
+ g_debug ("no match for %s: %s", xpath, error_local->message);
+
+ g_clear_error (&error_local);
+
+ /* For apps installed from .flatpak bundles there may not be any remote
+ * appstream data in @silo for it, so use the appstream data from
+ * within the app.
+ */
+ installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive),
+ gs_flatpak_app_get_ref_kind (app),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ cancellable,
+ &error_local);
+
+ if (installed_ref == NULL)
+ return !propagate_cancelled_error (error, &error_local); /* the app may not be installed */
+
+ appstream_gz = flatpak_installed_ref_load_appdata (installed_ref,
+ cancellable,
+ &error_local);
+ if (appstream_gz == NULL)
+ return !propagate_cancelled_error (error, &error_local);
+
+ g_debug ("using installed appdata for %s", gs_flatpak_app_get_ref_name (app));
+ return gs_flatpak_refine_appstream_from_bytes (self,
+ app,
+ flatpak_installed_ref_get_origin (installed_ref),
+ installed_ref,
+ appstream_gz,
+ flags,
+ interactive,
+ cancellable, error);
+ }
+
+ if (!gs_appstream_refine_app (self->plugin, app, silo, component, flags, error))
+ return FALSE;
+
+ /* use the default release as the version number */
+ gs_flatpak_refine_appstream_release (component, app);
+ return TRUE;
+}
+
+static gboolean
+gs_flatpak_refine_app_unlocked (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GRWLockReaderLocker **locker,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsAppState old_state = gs_app_get_state (app);
+ g_autoptr(GError) local_error = NULL;
+
+ /* not us */
+ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK)
+ return TRUE;
+
+ g_clear_pointer (locker, g_rw_lock_reader_locker_free);
+
+ if (!ensure_flatpak_silo_with_locker (self, locker, interactive, cancellable, error))
+ return FALSE;
+
+ /* always do AppStream properties */
+ if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, interactive, cancellable, error))
+ return FALSE;
+
+ /* AppStream sets the source to appname/arch/branch */
+ if (!gs_refine_item_metadata (self, app, error)) {
+ g_prefix_error (error, "failed to get metadata: ");
+ return FALSE;
+ }
+
+ /* check the installed state */
+ if (!gs_flatpak_refine_app_state_unlocked (self, app, interactive, cancellable, error)) {
+ g_prefix_error (error, "failed to get state: ");
+ return FALSE;
+ }
+
+ /* hide any addons that aren't for this app */
+ if (!gs_flatpak_prune_addons_list (self, app, interactive, cancellable, &local_error)) {
+ g_warning ("failed to prune addons: %s", local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ /* scope is fast, do unconditionally */
+ if (gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL)
+ gs_plugin_refine_item_scope (self, app);
+
+ /* if the state was changed, perhaps set the version from the release */
+ if (old_state != gs_app_get_state (app)) {
+ if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, interactive, cancellable, error))
+ return FALSE;
+ }
+
+ /* version fallback */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) {
+ if (gs_app_get_version (app) == NULL) {
+ const gchar *branch;
+ branch = gs_app_get_branch (app);
+ gs_app_set_version (app, branch);
+ }
+ }
+
+ /* size */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) {
+ g_autoptr(GError) error_local = NULL;
+ if (!gs_plugin_refine_item_size (self, app, interactive,
+ cancellable, &error_local)) {
+ if (g_error_matches (error_local, GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NO_NETWORK)) {
+ g_debug ("failed to get size while "
+ "refining app %s: %s",
+ gs_app_get_unique_id (app),
+ error_local->message);
+ } else {
+ g_prefix_error (&error_local, "failed to get size: ");
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+ }
+
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE_DATA) != 0 &&
+ gs_app_is_installed (app) &&
+ gs_app_get_kind (app) != AS_COMPONENT_KIND_RUNTIME) {
+ if (gs_app_get_size_cache_data (app, NULL) != GS_SIZE_TYPE_VALID)
+ gs_app_set_size_cache_data (app, GS_SIZE_TYPE_VALID,
+ gs_flatpak_get_app_directory_size (app, "cache", cancellable));
+ if (gs_app_get_size_user_data (app, NULL) != GS_SIZE_TYPE_VALID)
+ gs_app_set_size_user_data (app, GS_SIZE_TYPE_VALID,
+ gs_flatpak_get_app_directory_size (app, "config", cancellable) +
+ gs_flatpak_get_app_directory_size (app, "data", cancellable));
+
+ if (g_cancellable_is_cancelled (cancellable)) {
+ gs_app_set_size_cache_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ gs_app_set_size_user_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ }
+ }
+
+ /* origin-hostname */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) {
+ if (!gs_plugin_refine_item_origin_hostname (self, app, interactive,
+ cancellable,
+ error)) {
+ g_prefix_error (error, "failed to get origin-hostname: ");
+ return FALSE;
+ }
+ }
+
+ /* permissions */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME ||
+ flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) {
+ g_autoptr(GError) error_local = NULL;
+ if (!gs_plugin_refine_item_metadata (self, app, interactive,
+ cancellable, &error_local)) {
+ if (!gs_plugin_get_network_available (self->plugin) &&
+ g_error_matches (error_local, GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NO_NETWORK)) {
+ g_debug ("failed to get permissions while "
+ "refining app %s: %s",
+ gs_app_get_unique_id (app),
+ error_local->message);
+ } else {
+ g_prefix_error (&error_local, "failed to read permissions from app '%s' metadata: ", gs_app_get_unique_id (app));
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+ }
+
+ if (gs_app_get_origin (app))
+ gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, cancellable);
+
+ return TRUE;
+}
+
+void
+gs_flatpak_refine_addons (GsFlatpak *self,
+ GsApp *parent_app,
+ GsPluginRefineFlags flags,
+ GsAppState state,
+ gboolean interactive,
+ GCancellable *cancellable)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GsAppList) addons = NULL;
+ g_autoptr(GString) errors = NULL;
+ guint ii, sz;
+
+ addons = gs_app_dup_addons (parent_app);
+ sz = addons ? gs_app_list_length (addons) : 0;
+
+ for (ii = 0; ii < sz; ii++) {
+ GsApp *addon = gs_app_list_index (addons, ii);
+ g_autoptr(GError) local_error = NULL;
+
+ if (state != gs_app_get_state (addon))
+ continue;
+
+ /* To have refined also the state */
+ gs_app_set_state (addon, GS_APP_STATE_UNKNOWN);
+
+ if (!gs_flatpak_refine_app_unlocked (self, addon, flags, interactive, &locker, cancellable, &local_error)) {
+ if (errors)
+ g_string_append_c (errors, '\n');
+ else
+ errors = g_string_new (NULL);
+ g_string_append_printf (errors, _("Failed to refine addon ‘%s’: %s"),
+ gs_app_get_name (addon), local_error->message);
+ }
+ }
+
+ if (errors) {
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_autoptr(GError) error_local = g_error_new_literal (GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ errors->str);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (self->plugin, event);
+ }
+}
+
+gboolean
+gs_flatpak_refine_app (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+ return FALSE;
+
+ return gs_flatpak_refine_app_unlocked (self, app, flags, interactive, &locker, cancellable, error);
+}
+
+gboolean
+gs_flatpak_refine_wildcard (GsFlatpak *self, GsApp *app,
+ GsAppList *list, GsPluginRefineFlags refine_flags,
+ gboolean interactive,
+ GCancellable *cancellable, GError **error)
+{
+ const gchar *id;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ /* not enough info to find */
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ return TRUE;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ /* find all apps when matching any prefixes */
+ xpath = g_strdup_printf ("components/component/id[text()='%s']/..", 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;
+ }
+
+ gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) new = NULL;
+ new = gs_appstream_create_app (self->plugin, self->silo, component, error);
+ if (new == NULL)
+ return FALSE;
+ gs_flatpak_claim_app (self, new);
+ if (!gs_flatpak_refine_app_unlocked (self, new, refine_flags, interactive, &locker, cancellable, error))
+ return FALSE;
+ gs_app_subsume_metadata (new, app);
+ gs_app_list_add (list, new);
+ }
+
+ /* success */
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_launch (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* launch the app */
+ if (!flatpak_installation_launch (gs_flatpak_get_installation (self, interactive),
+ gs_flatpak_app_get_ref_name (app),
+ gs_flatpak_app_get_ref_arch (app),
+ gs_app_get_branch (app),
+ NULL,
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_app_remove_source (GsFlatpak *self,
+ GsApp *app,
+ gboolean is_remove,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ gboolean success;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* find the remote */
+ xremote = flatpak_installation_get_remote_by_name (installation,
+ gs_app_get_id (app),
+ cancellable, error);
+ if (xremote == NULL) {
+ gs_flatpak_error_convert (error);
+ g_prefix_error (error,
+ "flatpak source %s not found: ",
+ gs_app_get_id (app));
+ return FALSE;
+ }
+
+ /* remove */
+ gs_app_set_state (app, GS_APP_STATE_REMOVING);
+ if (is_remove) {
+ success = flatpak_installation_remove_remote (installation, gs_app_get_id (app), cancellable, error);
+ } else {
+ gboolean was_disabled = flatpak_remote_get_disabled (xremote);
+ flatpak_remote_set_disabled (xremote, TRUE);
+ success = flatpak_installation_modify_remote (installation, xremote, cancellable, error);
+ if (!success)
+ flatpak_remote_set_disabled (xremote, was_disabled);
+ }
+
+ if (!success) {
+ gs_flatpak_error_convert (error);
+ gs_app_set_state_recover (app);
+ return FALSE;
+ }
+
+ /* invalidate cache */
+ gs_flatpak_invalidate_silo (self);
+
+ gs_app_set_state (app, is_remove ? GS_APP_STATE_UNAVAILABLE : GS_APP_STATE_AVAILABLE);
+
+ gs_plugin_repository_changed (self->plugin, app);
+
+ return TRUE;
+}
+
+GsApp *
+gs_flatpak_file_to_app_bundle (GsFlatpak *self,
+ GFile *file,
+ gboolean unrefined,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GBytes) appstream_gz = NULL;
+ g_autoptr(GBytes) icon_data64 = NULL, icon_data128 = NULL;
+ g_autoptr(GBytes) metadata = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(FlatpakBundleRef) xref_bundle = NULL;
+
+ /* load bundle */
+ xref_bundle = flatpak_bundle_ref_new (file, error);
+ if (xref_bundle == NULL) {
+ gs_flatpak_error_convert (error);
+ g_prefix_error (error, "error loading bundle: ");
+ return NULL;
+ }
+
+ /* load metadata */
+ app = gs_flatpak_create_app (self, NULL, FLATPAK_REF (xref_bundle), NULL, interactive, cancellable);
+ if (unrefined)
+ return g_steal_pointer (&app);
+
+ gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_BUNDLE);
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL);
+ gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, flatpak_bundle_ref_get_installed_size (xref_bundle));
+ gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref_bundle));
+ metadata = flatpak_bundle_ref_get_metadata (xref_bundle);
+ if (!gs_flatpak_set_app_metadata (self, app,
+ g_bytes_get_data (metadata, NULL),
+ g_bytes_get_size (metadata),
+ interactive,
+ cancellable,
+ error))
+ return NULL;
+
+ /* load AppStream */
+ appstream_gz = flatpak_bundle_ref_get_appstream (xref_bundle);
+ if (appstream_gz != NULL) {
+ if (!gs_flatpak_refine_appstream_from_bytes (self, app, NULL, NULL,
+ appstream_gz,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ interactive,
+ cancellable, error))
+ return NULL;
+ } else {
+ g_warning ("no appstream metadata in file");
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST,
+ gs_flatpak_app_get_ref_name (app));
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST,
+ "A flatpak application");
+ gs_app_set_description (app, GS_APP_QUALITY_LOWEST, "");
+ }
+
+ /* Load icons. Currently flatpak only supports exactly 64px or 128px
+ * icons in bundles. */
+ icon_data64 = flatpak_bundle_ref_get_icon (xref_bundle, 64);
+ if (icon_data64 != NULL) {
+ g_autoptr(GIcon) icon = g_bytes_icon_new (icon_data64);
+ gs_icon_set_width (icon, 64);
+ gs_icon_set_height (icon, 64);
+ gs_app_add_icon (app, icon);
+ }
+
+ icon_data128 = flatpak_bundle_ref_get_icon (xref_bundle, 128);
+ if (icon_data128 != NULL) {
+ g_autoptr(GIcon) icon = g_bytes_icon_new (icon_data128);
+ gs_icon_set_width (icon, 128);
+ gs_icon_set_height (icon, 128);
+ gs_app_add_icon (app, icon);
+ }
+
+ /* Fallback */
+ if (icon_data64 == NULL && icon_data128 == NULL) {
+ g_autoptr(GIcon) icon = g_themed_icon_new ("system-component-application");
+ gs_app_add_icon (app, icon);
+ }
+
+ /* not quite true: this just means we can update this specific app */
+ if (flatpak_bundle_ref_get_origin (xref_bundle))
+ gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE);
+
+ /* success */
+ return g_steal_pointer (&app);
+}
+
+static gboolean
+_txn_abort_on_ready (FlatpakTransaction *transaction)
+{
+ return FALSE;
+}
+
+static gboolean
+_txn_add_new_remote (FlatpakTransaction *transaction,
+ FlatpakTransactionRemoteReason reason,
+ const char *from_id,
+ const char *remote_name,
+ const char *url)
+{
+ return TRUE;
+}
+
+static int
+_txn_choose_remote_for_ref (FlatpakTransaction *transaction,
+ const char *for_ref,
+ const char *runtime_ref,
+ const char * const *remotes)
+{
+ /* This transaction is just for displaying the app not installing it so
+ * this choice shouldn't matter */
+ return 0;
+}
+
+GsApp *
+gs_flatpak_file_to_app_ref (GsFlatpak *self,
+ GFile *file,
+ gboolean unrefined,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsApp *runtime;
+ const gchar *const *locales = g_get_language_names ();
+ const gchar *remote_name;
+ gboolean is_runtime, success;
+ gsize len = 0;
+ GList *txn_ops;
+#if !FLATPAK_CHECK_VERSION(1,13,1)
+ guint64 app_installed_size = 0, app_download_size = 0;
+#endif
+ g_autofree gchar *contents = NULL;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ g_autoptr(FlatpakRef) parsed_ref = NULL;
+ g_autoptr(FlatpakRemoteRef) remote_ref = NULL;
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ g_autoptr(GBytes) ref_file_data = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(XbBuilder) builder = xb_builder_new ();
+ g_autoptr(XbSilo) silo = NULL;
+ g_autofree gchar *origin_url = NULL;
+ g_autofree gchar *ref_comment = NULL;
+ g_autofree gchar *ref_description = NULL;
+ g_autofree gchar *ref_homepage = NULL;
+ g_autofree gchar *ref_icon = NULL;
+ g_autofree gchar *ref_title = NULL;
+ g_autofree gchar *ref_name = NULL;
+ g_autofree gchar *ref_branch = NULL;
+ FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive);
+
+ /* add current locales */
+ for (guint i = 0; locales[i] != NULL; i++)
+ xb_builder_add_locale (builder, locales[i]);
+
+ /* get file data */
+ if (!g_file_load_contents (file,
+ cancellable,
+ &contents,
+ &len,
+ NULL,
+ error)) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+
+ /* load the file */
+ kf = g_key_file_new ();
+ if (!g_key_file_load_from_data (kf, contents, len, G_KEY_FILE_NONE, error)) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+
+ /* check version */
+ if (g_key_file_has_key (kf, "Flatpak Ref", "Version", NULL)) {
+ guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Ref", "Version", NULL);
+ if (ver != 1) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "unsupported version %" G_GUINT64_FORMAT, ver);
+ return NULL;
+ }
+ }
+
+ /* get name, branch, kind */
+ ref_name = g_key_file_get_string (kf, "Flatpak Ref", "Name", error);
+ if (ref_name == NULL) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ if (g_key_file_has_key (kf, "Flatpak Ref", "Branch", NULL)) {
+ ref_branch = g_key_file_get_string (kf, "Flatpak Ref", "Branch", error);
+ if (ref_branch == NULL) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ } else {
+ ref_branch = g_strdup ("master");
+ }
+ if (g_key_file_has_key (kf, "Flatpak Ref", "IsRuntime", NULL)) {
+ is_runtime = g_key_file_get_boolean (kf, "Flatpak Ref", "IsRuntime", error);
+ if (error != NULL && *error != NULL) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ } else {
+ is_runtime = FALSE;
+ }
+
+ if (unrefined) {
+ /* Note: we don't support non-default arch here but it's not a
+ * regression since we never have for a flatpakref
+ */
+ g_autofree char *app_ref = g_strdup_printf ("%s/%s/%s/%s",
+ is_runtime ? "runtime" : "app",
+ ref_name,
+ flatpak_get_default_arch (),
+ ref_branch);
+ parsed_ref = flatpak_ref_parse (app_ref, error);
+ if (parsed_ref == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+
+ /* early return */
+ app = gs_flatpak_create_app (self, NULL, parsed_ref, NULL, interactive, cancellable);
+ return g_steal_pointer (&app);
+ }
+
+ /* Add the remote (to the temporary installation) but abort the
+ * transaction before it installs the app
+ */
+ transaction = flatpak_transaction_new_for_installation (installation, cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ flatpak_transaction_set_no_interaction (transaction, TRUE);
+ g_signal_connect (transaction, "ready-pre-auth", G_CALLBACK (_txn_abort_on_ready), NULL);
+ g_signal_connect (transaction, "add-new-remote", G_CALLBACK (_txn_add_new_remote), NULL);
+ g_signal_connect (transaction, "choose-remote-for-ref", G_CALLBACK (_txn_choose_remote_for_ref), NULL);
+ ref_file_data = g_bytes_new (contents, len);
+ if (!flatpak_transaction_add_install_flatpakref (transaction, ref_file_data, error)) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ success = flatpak_transaction_run (transaction, cancellable, &error_local);
+ g_assert (!success); /* aborted in _txn_abort_on_ready */
+
+ /* We don't check for FLATPAK_ERROR_ALREADY_INSTALLED here because it's
+ * a temporary installation
+ */
+ if (!g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_ABORTED)) {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+
+ g_clear_error (&error_local);
+
+ /* find the operation for the flatpakref */
+ txn_ops = flatpak_transaction_get_operations (transaction);
+ for (GList *l = txn_ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = l->data;
+ const char *op_ref = flatpak_transaction_operation_get_ref (op);
+ parsed_ref = flatpak_ref_parse (op_ref, error);
+ if (parsed_ref == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ if (g_strcmp0 (flatpak_ref_get_name (parsed_ref), ref_name) != 0) {
+ g_clear_object (&parsed_ref);
+ } else {
+ remote_name = flatpak_transaction_operation_get_remote (op);
+ g_debug ("auto-created remote name: %s", remote_name);
+#if !FLATPAK_CHECK_VERSION(1,13,1)
+ app_download_size = flatpak_transaction_operation_get_download_size (op);
+ app_installed_size = flatpak_transaction_operation_get_installed_size (op);
+#endif
+ break;
+ }
+ }
+ g_assert (parsed_ref != NULL);
+ g_list_free_full (g_steal_pointer (&txn_ops), g_object_unref);
+
+#if FLATPAK_CHECK_VERSION(1,13,1)
+ /* fetch remote ref */
+ remote_ref = flatpak_installation_fetch_remote_ref_sync (installation,
+ remote_name,
+ flatpak_ref_get_kind (parsed_ref),
+ flatpak_ref_get_name (parsed_ref),
+ flatpak_ref_get_arch (parsed_ref),
+ flatpak_ref_get_branch (parsed_ref),
+ cancellable,
+ error);
+ if (remote_ref == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ app = gs_flatpak_create_app (self, remote_name, FLATPAK_REF (remote_ref), NULL, interactive, cancellable);
+#else
+ app = gs_flatpak_create_app (self, remote_name, parsed_ref, NULL, interactive, cancellable);
+ gs_app_set_size_download (app, (app_download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, app_download_size);
+ gs_app_set_size_installed (app, (app_installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, app_installed_size);
+#endif
+
+ gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE);
+ gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REF);
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+
+ runtime = gs_app_get_runtime (app);
+ if (runtime != NULL) {
+ g_autofree char *runtime_ref = gs_flatpak_app_get_ref_display (runtime);
+ if (gs_app_get_state (runtime) == GS_APP_STATE_UNKNOWN) {
+ g_autofree gchar *uri = NULL;
+ /* the new runtime is available from the RuntimeRepo */
+ uri = g_key_file_get_string (kf, "Flatpak Ref", "RuntimeRepo", NULL);
+ gs_flatpak_app_set_runtime_url (runtime, uri);
+ }
+
+ /* find the operation for the runtime to set its size data. Since this
+ * is all happening on a tmp installation, it won't be available later
+ * during the refine step
+ */
+ txn_ops = flatpak_transaction_get_operations (transaction);
+ for (GList *l = txn_ops; l != NULL; l = l->next) {
+ FlatpakTransactionOperation *op = l->data;
+ const char *op_ref = flatpak_transaction_operation_get_ref (op);
+ if (g_strcmp0 (runtime_ref, op_ref) == 0) {
+ guint64 installed_size = 0, download_size = 0;
+ download_size = flatpak_transaction_operation_get_download_size (op);
+ gs_app_set_size_download (runtime, (download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, download_size);
+ installed_size = flatpak_transaction_operation_get_installed_size (op);
+ gs_app_set_size_installed (runtime, (installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, installed_size);
+ break;
+ }
+ }
+ g_list_free_full (g_steal_pointer (&txn_ops), g_object_unref);
+ }
+
+ /* use the data from the flatpakref file as a fallback */
+ ref_title = g_key_file_get_string (kf, "Flatpak Ref", "Title", NULL);
+ if (ref_title != NULL)
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, ref_title);
+ ref_comment = g_key_file_get_string (kf, "Flatpak Ref", "Comment", NULL);
+ if (ref_comment != NULL)
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, ref_comment);
+ ref_description = g_key_file_get_string (kf, "Flatpak Ref", "Description", NULL);
+ if (ref_description != NULL)
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, ref_description);
+ ref_homepage = g_key_file_get_string (kf, "Flatpak Ref", "Homepage", NULL);
+ if (ref_homepage != NULL)
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, ref_homepage);
+ ref_icon = g_key_file_get_string (kf, "Flatpak Ref", "Icon", NULL);
+ if (ref_icon != NULL &&
+ (g_str_has_prefix (ref_icon, "http:") ||
+ g_str_has_prefix (ref_icon, "https:"))) {
+ g_autoptr(GIcon) icon = gs_remote_icon_new (ref_icon);
+ gs_app_add_icon (app, icon);
+ }
+
+ /* set the origin data */
+ xremote = flatpak_installation_get_remote_by_name (installation,
+ remote_name,
+ cancellable,
+ error);
+ if (xremote == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ origin_url = flatpak_remote_get_url (xremote);
+ if (origin_url == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "no URL for remote %s",
+ flatpak_remote_get_name (xremote));
+ return NULL;
+ }
+ gs_app_set_origin_hostname (app, origin_url);
+
+ /* get the new appstream data (nonfatal for failure) */
+ if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive,
+ cancellable, &error_local)) {
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ gs_flatpak_error_convert (&error_local);
+
+ event = gs_plugin_event_new ("app", app,
+ "error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (self->plugin, event);
+ g_clear_error (&error_local);
+ }
+
+ /* get this now, as it's not going to be available at install time */
+ if (!gs_plugin_refine_item_metadata (self, app, interactive, cancellable, error))
+ return NULL;
+
+ /* parse it */
+ if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, interactive, cancellable, error))
+ return NULL;
+
+ /* build silo */
+ /* No need to change the thread-default main context because the silo
+ * doesn’t live beyond this function */
+ silo = xb_builder_compile (builder,
+ XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+ cancellable,
+ error);
+ if (silo == NULL)
+ return NULL;
+ if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
+ g_autofree gchar *xml = NULL;
+ xml = xb_silo_export (silo,
+ XB_NODE_EXPORT_FLAG_FORMAT_INDENT |
+ XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE,
+ NULL);
+ g_debug ("showing AppStream data: %s", xml);
+ }
+
+ /* get extra AppStream data if available */
+ if (!gs_flatpak_refine_appstream (self, app, silo,
+ GS_PLUGIN_REFINE_FLAGS_MASK,
+ interactive,
+ cancellable,
+ error))
+ return NULL;
+
+ /* success */
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_flatpak_search (GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GMutexLocker) app_silo_locker = NULL;
+ g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new ();
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_search (self->plugin, self->silo, values, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+ gs_flatpak_claim_app_list (self, list_tmp, interactive);
+ gs_app_list_add_list (list, list_tmp);
+
+ /* Also search silos from installed apps which were missing from self->silo */
+ app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex);
+ g_hash_table_iter_init (&iter, self->app_silos);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ g_autoptr(XbSilo) app_silo = g_object_ref (value);
+ g_autoptr(GsAppList) app_list_tmp = gs_app_list_new ();
+ const char *app_ref = (char *)key;
+ g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+ g_auto(GStrv) split = NULL;
+ FlatpakRefKind kind;
+
+ /* Ignore any silos of apps that have since been removed.
+ * FIXME: can we use self->installed_refs here? */
+ split = g_strsplit (app_ref, "/", -1);
+ g_assert (g_strv_length (split) == 4);
+ if (g_strcmp0 (split[0], "app") == 0)
+ kind = FLATPAK_REF_KIND_APP;
+ else
+ kind = FLATPAK_REF_KIND_RUNTIME;
+ installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive),
+ kind,
+ split[1],
+ split[2],
+ split[3],
+ NULL, NULL);
+ if (installed_ref == NULL) {
+ g_ptr_array_add (silos_to_remove, (gpointer) app_ref);
+ continue;
+ }
+
+ if (!gs_appstream_search (self->plugin, app_silo, values, app_list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, app_list_tmp, interactive);
+ gs_app_list_add_list (list, app_list_tmp);
+ }
+
+ for (guint i = 0; i < silos_to_remove->len; i++) {
+ const char *silo = g_ptr_array_index (silos_to_remove, i);
+ g_hash_table_remove (self->app_silos, silo);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_search_developer_apps (GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ g_autoptr(GMutexLocker) app_silo_locker = NULL;
+ g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new ();
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_search_developer_apps (self->plugin, self->silo, values, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+ gs_flatpak_claim_app_list (self, list_tmp, interactive);
+ gs_app_list_add_list (list, list_tmp);
+
+ /* Also search silos from installed apps which were missing from self->silo */
+ app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex);
+ g_hash_table_iter_init (&iter, self->app_silos);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ g_autoptr(XbSilo) app_silo = g_object_ref (value);
+ g_autoptr(GsAppList) app_list_tmp = gs_app_list_new ();
+ const char *app_ref = (char *)key;
+ g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+ g_auto(GStrv) split = NULL;
+ FlatpakRefKind kind;
+
+ /* Ignore any silos of apps that have since been removed.
+ * FIXME: can we use self->installed_refs here? */
+ split = g_strsplit (app_ref, "/", -1);
+ g_assert (g_strv_length (split) == 4);
+ if (g_strcmp0 (split[0], "app") == 0)
+ kind = FLATPAK_REF_KIND_APP;
+ else
+ kind = FLATPAK_REF_KIND_RUNTIME;
+ installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive),
+ kind,
+ split[1],
+ split[2],
+ split[3],
+ NULL, NULL);
+ if (installed_ref == NULL) {
+ g_ptr_array_add (silos_to_remove, (gpointer) app_ref);
+ continue;
+ }
+
+ if (!gs_appstream_search_developer_apps (self->plugin, app_silo, values, app_list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, app_list_tmp, interactive);
+ gs_app_list_add_list (list, app_list_tmp);
+ }
+
+ for (guint i = 0; i < silos_to_remove->len; i++) {
+ const char *silo = g_ptr_array_index (silos_to_remove, i);
+ g_hash_table_remove (self->app_silos, silo);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_category_apps (GsFlatpak *self,
+ GsCategory *category,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ return gs_appstream_add_category_apps (self->plugin, self->silo,
+ category, list,
+ cancellable, error);
+}
+
+gboolean
+gs_flatpak_refine_category_sizes (GsFlatpak *self,
+ GPtrArray *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ return gs_appstream_refine_category_sizes (self->silo, list, cancellable, error);
+}
+
+gboolean
+gs_flatpak_add_popular (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_add_popular (self->silo, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_featured (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_add_featured (self->silo, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_deployment_featured (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ const gchar *const *deployments,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ return gs_appstream_add_deployment_featured (self->silo, deployments, list, cancellable, error);
+}
+
+gboolean
+gs_flatpak_add_alternates (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_add_alternates (self->silo, app, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_recent (GsFlatpak *self,
+ GsAppList *list,
+ guint64 age,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_add_recent (self->plugin, self->silo, list_tmp, age,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, list_tmp, interactive);
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_url_to_app (GsFlatpak *self,
+ GsAppList *list,
+ const gchar *url,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error))
+ return FALSE;
+
+ if (!gs_appstream_url_to_app (self->plugin, self->silo, list_tmp, url, cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, list_tmp, interactive);
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+const gchar *
+gs_flatpak_get_id (GsFlatpak *self)
+{
+ if (self->id == NULL) {
+ GString *str = g_string_new ("flatpak");
+ g_string_append_printf (str, "-%s",
+ as_component_scope_to_string (self->scope));
+ if (flatpak_installation_get_id (self->installation_noninteractive) != NULL) {
+ g_string_append_printf (str, "-%s",
+ flatpak_installation_get_id (self->installation_noninteractive));
+ }
+ if (self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY)
+ g_string_append (str, "-temp");
+ self->id = g_string_free (str, FALSE);
+ }
+ return self->id;
+}
+
+AsComponentScope
+gs_flatpak_get_scope (GsFlatpak *self)
+{
+ return self->scope;
+}
+
+FlatpakInstallation *
+gs_flatpak_get_installation (GsFlatpak *self,
+ gboolean interactive)
+{
+ return interactive ? self->installation_interactive : self->installation_noninteractive;
+}
+
+static void
+gs_flatpak_finalize (GObject *object)
+{
+ GsFlatpak *self;
+ g_return_if_fail (GS_IS_FLATPAK (object));
+ self = GS_FLATPAK (object);
+
+ if (self->changed_id > 0) {
+ g_signal_handler_disconnect (self->monitor, self->changed_id);
+ self->changed_id = 0;
+ }
+ if (self->silo != NULL)
+ g_object_unref (self->silo);
+ if (self->monitor != NULL)
+ g_object_unref (self->monitor);
+
+ g_free (self->id);
+ g_object_unref (self->installation_noninteractive);
+ g_object_unref (self->installation_interactive);
+ g_clear_pointer (&self->installed_refs, g_ptr_array_unref);
+ g_mutex_clear (&self->installed_refs_mutex);
+ g_object_unref (self->plugin);
+ g_hash_table_unref (self->broken_remotes);
+ g_mutex_clear (&self->broken_remotes_mutex);
+ g_rw_lock_clear (&self->silo_lock);
+ g_hash_table_unref (self->app_silos);
+ g_mutex_clear (&self->app_silos_mutex);
+ g_clear_pointer (&self->remote_title, g_hash_table_unref);
+ g_mutex_clear (&self->remote_title_mutex);
+
+ G_OBJECT_CLASS (gs_flatpak_parent_class)->finalize (object);
+}
+
+static void
+gs_flatpak_class_init (GsFlatpakClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_flatpak_finalize;
+}
+
+static void
+gs_flatpak_init (GsFlatpak *self)
+{
+ /* XbSilo needs external locking as we destroy the silo and build a new
+ * one when something changes */
+ g_rw_lock_init (&self->silo_lock);
+
+ g_mutex_init (&self->installed_refs_mutex);
+ self->installed_refs = NULL;
+ g_mutex_init (&self->broken_remotes_mutex);
+ self->broken_remotes = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+ self->app_silos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+ g_mutex_init (&self->app_silos_mutex);
+ self->remote_title = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ g_mutex_init (&self->remote_title_mutex);
+}
+
+GsFlatpak *
+gs_flatpak_new (GsPlugin *plugin, FlatpakInstallation *installation, GsFlatpakFlags flags)
+{
+ GsFlatpak *self;
+ g_autoptr(GFile) path = NULL;
+ gboolean is_user;
+
+ path = flatpak_installation_get_path (installation);
+ is_user = flatpak_installation_get_is_user (installation);
+
+ self = g_object_new (GS_TYPE_FLATPAK, NULL);
+
+ self->installation_noninteractive = g_object_ref (installation);
+ flatpak_installation_set_no_interaction (self->installation_noninteractive, TRUE);
+
+ /* Cloning it should never fail as the repo should already exist on disk. */
+ self->installation_interactive = flatpak_installation_new_for_path (path, is_user, NULL, NULL);
+ g_assert (self->installation_interactive != NULL);
+ flatpak_installation_set_no_interaction (self->installation_interactive, FALSE);
+
+ self->scope = is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM;
+ self->plugin = g_object_ref (plugin);
+ self->flags = flags;
+ return GS_FLATPAK (self);
+}
+
+void
+gs_flatpak_set_busy (GsFlatpak *self,
+ gboolean busy)
+{
+ g_return_if_fail (GS_IS_FLATPAK (self));
+
+ if (busy) {
+ g_atomic_int_inc (&self->busy);
+ } else {
+ g_return_if_fail (g_atomic_int_get (&self->busy) > 0);
+ if (g_atomic_int_dec_and_test (&self->busy)) {
+ if (self->changed_while_busy) {
+ self->changed_while_busy = FALSE;
+ g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, gs_flatpak_claim_changed_idle_cb,
+ g_object_ref (self), g_object_unref);
+ }
+ }
+ }
+}
+
+gboolean
+gs_flatpak_get_busy (GsFlatpak *self)
+{
+ g_return_val_if_fail (GS_IS_FLATPAK (self), FALSE);
+ return g_atomic_int_get (&self->busy) > 0;
+}
diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h
new file mode 100644
index 0000000..b3f8a13
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak.h
@@ -0,0 +1,184 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com>
+ * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gnome-software.h>
+#include <flatpak.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_FLATPAK (gs_flatpak_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsFlatpak, gs_flatpak, GS, FLATPAK, GObject)
+
+typedef enum {
+ GS_FLATPAK_FLAG_NONE = 0,
+ GS_FLATPAK_FLAG_IS_TEMPORARY = 1 << 0,
+ GS_FLATPAK_FLAG_LAST /*< skip >*/
+} GsFlatpakFlags;
+
+GsFlatpak *gs_flatpak_new (GsPlugin *plugin,
+ FlatpakInstallation *installation,
+ GsFlatpakFlags flags);
+FlatpakInstallation *gs_flatpak_get_installation (GsFlatpak *self,
+ gboolean interactive);
+
+GsApp *gs_flatpak_ref_to_app (GsFlatpak *self,
+ const gchar *ref,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+
+AsComponentScope gs_flatpak_get_scope (GsFlatpak *self);
+const gchar *gs_flatpak_get_id (GsFlatpak *self);
+gboolean gs_flatpak_setup (GsFlatpak *self,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_installed (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_sources (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_updates (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refresh (GsFlatpak *self,
+ guint64 cache_age_secs,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_app (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+void gs_flatpak_refine_addons (GsFlatpak *self,
+ GsApp *parent_app,
+ GsPluginRefineFlags flags,
+ GsAppState state,
+ gboolean interactive,
+ GCancellable *cancellable);
+gboolean gs_flatpak_refine_app_state (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_wildcard (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_launch (GsFlatpak *self,
+ GsApp *app,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_app_remove_source (GsFlatpak *self,
+ GsApp *app,
+ gboolean is_remove,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_app_install_source (GsFlatpak *self,
+ GsApp *app,
+ gboolean is_install,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_file_to_app_ref (GsFlatpak *self,
+ GFile *file,
+ gboolean unrefined,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_file_to_app_bundle (GsFlatpak *self,
+ GFile *file,
+ gboolean unrefined,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_find_source_by_url (GsFlatpak *self,
+ const gchar *name,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_search (GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_search_developer_apps(GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_category_sizes(GsFlatpak *self,
+ GPtrArray *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_category_apps (GsFlatpak *self,
+ GsCategory *category,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_popular (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_featured (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_deployment_featured
+ (GsFlatpak *self,
+ GsAppList *list,
+ gboolean interactive,
+ const gchar *const *deployments,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_alternates (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_recent (GsFlatpak *self,
+ GsAppList *list,
+ guint64 age,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_url_to_app (GsFlatpak *self,
+ GsAppList *list,
+ const gchar *url,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error);
+void gs_flatpak_set_busy (GsFlatpak *self,
+ gboolean busy);
+gboolean gs_flatpak_get_busy (GsFlatpak *self);
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-plugin-flatpak.c b/plugins/flatpak/gs-plugin-flatpak.c
new file mode 100644
index 0000000..7c893ef
--- /dev/null
+++ b/plugins/flatpak/gs-plugin-flatpak.c
@@ -0,0 +1,2326 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com>
+ * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/*
+ * SECTION:
+ * Exposes flatpaks from the user and system repositories.
+ *
+ * All GsApp's created have management-plugin set to flatpak
+ * Some GsApp's created have have flatpak::kind of app or runtime
+ * The GsApp:origin is the remote name, e.g. test-repo
+ *
+ * The plugin has a worker thread which all operations are delegated to, as the
+ * libflatpak API is entirely synchronous (and thread-safe). * Message passing
+ * to the worker thread is by gs_worker_thread_queue().
+ *
+ * FIXME: It may speed things up in future to have one worker thread *per*
+ * `FlatpakInstallation`, all operating in parallel.
+ */
+
+#include <config.h>
+
+#include <flatpak.h>
+#include <glib/gi18n.h>
+#include <gnome-software.h>
+
+#include "gs-appstream.h"
+#include "gs-flatpak-app.h"
+#include "gs-flatpak.h"
+#include "gs-flatpak-transaction.h"
+#include "gs-flatpak-utils.h"
+#include "gs-metered.h"
+#include "gs-worker-thread.h"
+
+#include "gs-plugin-flatpak.h"
+
+struct _GsPluginFlatpak
+{
+ GsPlugin parent;
+
+ GsWorkerThread *worker; /* (owned) */
+
+ GPtrArray *installations; /* (element-type GsFlatpak) (owned); may be NULL before setup or after shutdown */
+ gboolean has_system_helper;
+ const gchar *destdir_for_tests;
+};
+
+G_DEFINE_TYPE (GsPluginFlatpak, gs_plugin_flatpak, GS_TYPE_PLUGIN)
+
+#define assert_in_worker(self) \
+ g_assert (gs_worker_thread_is_in_worker_context (self->worker))
+
+/* Work around flatpak_transaction_get_no_interaction() not existing before
+ * flatpak 1.13.0. */
+#if !FLATPAK_CHECK_VERSION(1,13,0)
+#define flatpak_transaction_get_no_interaction(transaction) \
+ GPOINTER_TO_INT (g_object_get_data (G_OBJECT (transaction), "flatpak-no-interaction"))
+#define flatpak_transaction_set_no_interaction(transaction, no_interaction) \
+ G_STMT_START { \
+ FlatpakTransaction *ftsni_transaction = (transaction); \
+ gboolean ftsni_no_interaction = (no_interaction); \
+ (flatpak_transaction_set_no_interaction) (ftsni_transaction, ftsni_no_interaction); \
+ g_object_set_data (G_OBJECT (ftsni_transaction), "flatpak-no-interaction", GINT_TO_POINTER (ftsni_no_interaction)); \
+ } G_STMT_END
+#endif /* flatpak < 1.13.0 */
+
+static void
+gs_plugin_flatpak_dispose (GObject *object)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (object);
+
+ g_clear_pointer (&self->installations, g_ptr_array_unref);
+ g_clear_object (&self->worker);
+
+ G_OBJECT_CLASS (gs_plugin_flatpak_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_flatpak_init (GsPluginFlatpak *self)
+{
+ GsPlugin *plugin = GS_PLUGIN (self);
+
+ self->installations = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+
+ /* getting app properties from appstream is quicker */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+
+ /* like appstream, we need the icon plugin to load cached icons into pixbufs */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons");
+
+ /* prioritize over packages */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit");
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "rpm-ostree");
+
+ /* set name of MetaInfo file */
+ gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Flatpak");
+
+ /* used for self tests */
+ self->destdir_for_tests = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR");
+}
+
+static gboolean
+_as_component_scope_is_compatible (AsComponentScope scope1, AsComponentScope scope2)
+{
+ if (scope1 == AS_COMPONENT_SCOPE_UNKNOWN)
+ return TRUE;
+ if (scope2 == AS_COMPONENT_SCOPE_UNKNOWN)
+ return TRUE;
+ return scope1 == scope2;
+}
+
+void
+gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app)
+{
+ if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK)
+ gs_app_set_management_plugin (app, plugin);
+}
+
+static gboolean
+gs_plugin_flatpak_add_installation (GsPluginFlatpak *self,
+ FlatpakInstallation *installation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsFlatpak) flatpak = NULL;
+
+ /* create and set up */
+ flatpak = gs_flatpak_new (GS_PLUGIN (self), installation, GS_FLATPAK_FLAG_NONE);
+ if (!gs_flatpak_setup (flatpak, cancellable, error))
+ return FALSE;
+ g_debug ("successfully set up %s", gs_flatpak_get_id (flatpak));
+
+ /* add objects that set up correctly */
+ g_ptr_array_add (self->installations, g_steal_pointer (&flatpak));
+ return TRUE;
+}
+
+static void
+gs_plugin_flatpak_report_warning (GsPlugin *plugin,
+ GError **error)
+{
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_assert (error != NULL);
+ if (*error != NULL && (*error)->domain != GS_PLUGIN_ERROR)
+ gs_flatpak_error_convert (error);
+
+ event = gs_plugin_event_new ("error", *error,
+ NULL);
+ gs_plugin_event_add_flag (event,
+ GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+}
+
+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_flatpak_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ g_debug ("Flatpak version: %d.%d.%d",
+ FLATPAK_MAJOR_VERSION,
+ FLATPAK_MINOR_VERSION,
+ FLATPAK_MICRO_VERSION);
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_setup_async);
+
+ /* Shouldn’t end up setting up twice */
+ g_assert (self->installations == NULL || self->installations->len == 0);
+
+ /* Start up a worker thread to process all the plugin’s function calls. */
+ self->worker = gs_worker_thread_new ("gs-plugin-flatpak");
+
+ /* Queue a job to find and set up the installations. */
+ 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)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsPlugin *plugin = GS_PLUGIN (self);
+ g_autoptr(GPtrArray) installations = NULL;
+ const gchar *action_id = "org.freedesktop.Flatpak.appstream-update";
+ g_autoptr(GError) permission_error = NULL;
+ g_autoptr(GPermission) permission = NULL;
+
+ assert_in_worker (self);
+
+ /* if we can't update the AppStream database system-wide don't even
+ * pull the data as we can't do anything with it */
+ permission = gs_utils_get_permission (action_id, NULL, &permission_error);
+ if (permission == NULL) {
+ g_debug ("no permission for %s: %s", action_id, permission_error->message);
+ g_clear_error (&permission_error);
+ } else {
+ self->has_system_helper = g_permission_get_allowed (permission) ||
+ g_permission_get_can_acquire (permission);
+ }
+
+ /* if we're not just running the tests */
+ if (self->destdir_for_tests == NULL) {
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(FlatpakInstallation) installation = NULL;
+
+ /* include the system installations */
+ if (self->has_system_helper) {
+ installations = flatpak_get_system_installations (cancellable,
+ &error_local);
+
+ if (installations == NULL) {
+ gs_plugin_flatpak_report_warning (plugin, &error_local);
+ g_clear_error (&error_local);
+ }
+ }
+
+ /* include the user installation */
+ installation = flatpak_installation_new_user (cancellable,
+ &error_local);
+ if (installation == NULL) {
+ /* if some error happened, report it as an event, but
+ * do not return it, otherwise it will disable the whole
+ * plugin (meaning that support for Flatpak will not be
+ * possible even if a system installation is working) */
+ gs_plugin_flatpak_report_warning (plugin, &error_local);
+ } else {
+ if (installations == NULL)
+ installations = g_ptr_array_new_with_free_func (g_object_unref);
+
+ g_ptr_array_add (installations, g_steal_pointer (&installation));
+ }
+ } else {
+ g_autoptr(GError) error_local = NULL;
+
+ /* use the test installation */
+ g_autofree gchar *full_path = g_build_filename (self->destdir_for_tests,
+ "flatpak",
+ NULL);
+ g_autoptr(GFile) file = g_file_new_for_path (full_path);
+ g_autoptr(FlatpakInstallation) installation = NULL;
+ g_debug ("using custom flatpak path %s", full_path);
+ installation = flatpak_installation_new_for_path (file, TRUE,
+ cancellable,
+ &error_local);
+ if (installation == NULL) {
+ gs_flatpak_error_convert (&error_local);
+ g_task_return_error (task, g_steal_pointer (&error_local));
+ return;
+ }
+
+ installations = g_ptr_array_new_with_free_func (g_object_unref);
+ g_ptr_array_add (installations, g_steal_pointer (&installation));
+ }
+
+ /* add the installations */
+ for (guint i = 0; installations != NULL && i < installations->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+
+ FlatpakInstallation *installation = g_ptr_array_index (installations, i);
+ if (!gs_plugin_flatpak_add_installation (self,
+ installation,
+ cancellable,
+ &error_local)) {
+ gs_plugin_flatpak_report_warning (plugin,
+ &error_local);
+ continue;
+ }
+ }
+
+ /* when no installation has been loaded, return the error so the
+ * plugin gets disabled */
+ if (self->installations->len == 0) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ "Failed to load any Flatpak installations");
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_flatpak_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_flatpak_shutdown_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_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);
+ GsPluginFlatpak *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;
+ }
+
+ /* Clear the flatpak installations */
+ g_ptr_array_set_size (self->installations, 0);
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_flatpak_shutdown_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+gs_plugin_add_sources (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ if (!gs_flatpak_add_sources (flatpak, list, interactive, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_updates (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ g_autoptr(GError) local_error = NULL;
+ if (!gs_flatpak_add_updates (flatpak, list, interactive, cancellable, &local_error))
+ g_debug ("Failed to get updates for '%s': %s", gs_flatpak_get_id (flatpak), local_error->message);
+ }
+ gs_plugin_cache_lookup_by_state (plugin, list, GS_APP_STATE_INSTALLING);
+ return TRUE;
+}
+
+static void refresh_metadata_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_refresh_metadata_async (GsPlugin *plugin,
+ guint64 cache_age_secs,
+ GsPluginRefreshMetadataFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (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_flatpak_refresh_metadata_async);
+ g_task_set_task_data (task, gs_plugin_refresh_metadata_data_new (cache_age_secs, flags), (GDestroyNotify) gs_plugin_refresh_metadata_data_free);
+
+ /* Queue a job to get the installed apps. */
+ 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)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsPluginRefreshMetadataData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE);
+
+ assert_in_worker (self);
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ g_autoptr(GError) local_error = NULL;
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+
+ if (!gs_flatpak_refresh (flatpak, data->cache_age_secs, interactive, cancellable, &local_error))
+ g_debug ("Failed to refresh metadata for '%s': %s", gs_flatpak_get_id (flatpak), local_error->message);
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_flatpak_refresh_metadata_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static GsFlatpak *
+gs_plugin_flatpak_get_handler (GsPluginFlatpak *self,
+ GsApp *app)
+{
+ const gchar *object_id;
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
+ return NULL;
+
+ /* specified an explicit name */
+ object_id = gs_flatpak_app_get_object_id (app);
+ if (object_id != NULL) {
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ if (g_strcmp0 (gs_flatpak_get_id (flatpak), object_id) == 0)
+ return flatpak;
+ }
+ }
+
+ /* find a scope that matches */
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ if (_as_component_scope_is_compatible (gs_flatpak_get_scope (flatpak),
+ gs_app_get_scope (app)))
+ return flatpak;
+ }
+ return NULL;
+}
+
+static gboolean
+gs_plugin_flatpak_refine_app (GsPluginFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsFlatpak *flatpak = NULL;
+
+ /* not us */
+ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) {
+ g_debug ("%s not a package, ignoring", gs_app_get_unique_id (app));
+ return TRUE;
+ }
+
+ /* we have to look for the app in all GsFlatpak stores */
+ if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) {
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak_tmp = g_ptr_array_index (self->installations, i);
+ g_autoptr(GError) error_local = NULL;
+ if (gs_flatpak_refine_app_state (flatpak_tmp, app, interactive,
+ cancellable, &error_local)) {
+ flatpak = flatpak_tmp;
+ break;
+ } else {
+ g_debug ("%s", error_local->message);
+ }
+ }
+ } else {
+ flatpak = gs_plugin_flatpak_get_handler (self, app);
+ }
+ if (flatpak == NULL)
+ return TRUE;
+ return gs_flatpak_refine_app (flatpak, app, flags, interactive, cancellable, error);
+}
+
+
+static gboolean
+refine_app (GsPluginFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
+ return TRUE;
+
+ /* get the runtime first */
+ if (!gs_plugin_flatpak_refine_app (self, app, flags, interactive, cancellable, error))
+ return FALSE;
+
+ /* the runtime might be installed in a different scope */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) {
+ GsApp *runtime = gs_app_get_runtime (app);
+ if (runtime != NULL) {
+ if (!gs_plugin_flatpak_refine_app (self, runtime,
+ flags,
+ interactive,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+ return TRUE;
+}
+
+static void refine_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (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_flatpak_refine_async);
+
+ /* Queue a job to refine the apps. */
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ 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)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsPluginRefineData *data = task_data;
+ GsAppList *list = data->list;
+ GsPluginRefineFlags flags = data->flags;
+ gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
+ g_autoptr(GsAppList) app_list = NULL;
+ 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, interactive, cancellable, &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))
+ continue;
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+
+ if (!gs_flatpak_refine_wildcard (flatpak, app, list, flags, interactive,
+ cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_flatpak_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+gs_plugin_launch (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (GS_PLUGIN_FLATPAK (plugin), app);
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ if (flatpak == NULL)
+ return TRUE;
+
+ return gs_flatpak_launch (flatpak, app, interactive, cancellable, error);
+}
+
+/* ref full */
+static GsApp *
+gs_plugin_flatpak_find_app_by_ref (GsPluginFlatpak *self,
+ const gchar *ref,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_debug ("finding ref %s", ref);
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak_tmp = g_ptr_array_index (self->installations, i);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ app = gs_flatpak_ref_to_app (flatpak_tmp, ref, interactive, cancellable, &error_local);
+ if (app == NULL) {
+ g_debug ("%s", error_local->message);
+ continue;
+ }
+ g_debug ("found ref=%s->%s", ref, gs_app_get_unique_id (app));
+ return g_steal_pointer (&app);
+ }
+ return NULL;
+}
+
+/* ref full */
+static GsApp *
+_ref_to_app (FlatpakTransaction *transaction,
+ const gchar *ref,
+ GsPluginFlatpak *self)
+{
+ g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction), NULL);
+ g_return_val_if_fail (ref != NULL, NULL);
+ g_return_val_if_fail (GS_IS_PLUGIN_FLATPAK (self), NULL);
+
+ /* search through each GsFlatpak */
+ return gs_plugin_flatpak_find_app_by_ref (self, ref,
+ gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE),
+ NULL, NULL);
+}
+
+static void
+_group_apps_by_installation_recurse (GsPluginFlatpak *self,
+ GsAppList *list,
+ GHashTable *applist_by_flatpaks)
+{
+ if (!list)
+ return;
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (self, app);
+ if (flatpak != NULL) {
+ GsAppList *list_tmp = g_hash_table_lookup (applist_by_flatpaks, flatpak);
+ GsAppList *related_list;
+ if (list_tmp == NULL) {
+ list_tmp = gs_app_list_new ();
+ g_hash_table_insert (applist_by_flatpaks,
+ g_object_ref (flatpak),
+ list_tmp);
+ }
+ gs_app_list_add (list_tmp, app);
+
+ /* Add also related apps, which can be those recognized for update,
+ while the 'app' is already up to date. */
+ related_list = gs_app_get_related (app);
+ _group_apps_by_installation_recurse (self, related_list, applist_by_flatpaks);
+ }
+ }
+}
+
+/*
+ * Returns: (transfer full) (element-type GsFlatpak GsAppList):
+ * a map from GsFlatpak to non-empty lists of apps from @list associated
+ * with that installation.
+ */
+static GHashTable *
+_group_apps_by_installation (GsPluginFlatpak *self,
+ GsAppList *list)
+{
+ g_autoptr(GHashTable) applist_by_flatpaks = NULL;
+
+ /* list of apps to be handled by each flatpak installation */
+ applist_by_flatpaks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+ (GDestroyNotify) g_object_unref,
+ (GDestroyNotify) g_object_unref);
+
+ /* put each app into the correct per-GsFlatpak list */
+ _group_apps_by_installation_recurse (self, list, applist_by_flatpaks);
+
+ return g_steal_pointer (&applist_by_flatpaks);
+}
+
+typedef struct {
+ FlatpakTransaction *transaction;
+ guint id;
+} BasicAuthData;
+
+static void
+basic_auth_data_free (BasicAuthData *data)
+{
+ g_object_unref (data->transaction);
+ g_slice_free (BasicAuthData, data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(BasicAuthData, basic_auth_data_free)
+
+static void
+_basic_auth_cb (const gchar *user, const gchar *password, gpointer user_data)
+{
+ g_autoptr(BasicAuthData) data = user_data;
+
+ g_debug ("Submitting basic auth data");
+
+ /* NULL user aborts the basic auth request */
+ flatpak_transaction_complete_basic_auth (data->transaction, data->id, user, password, NULL /* options */);
+}
+
+static gboolean
+_basic_auth_start (FlatpakTransaction *transaction,
+ const char *remote,
+ const char *realm,
+ GVariant *options,
+ guint id,
+ GsPlugin *plugin)
+{
+ BasicAuthData *data;
+
+ if (flatpak_transaction_get_no_interaction (transaction))
+ return FALSE;
+
+ data = g_slice_new0 (BasicAuthData);
+ data->transaction = g_object_ref (transaction);
+ data->id = id;
+
+ g_debug ("Login required remote %s (realm %s)\n", remote, realm);
+ gs_plugin_basic_auth_start (plugin, remote, realm, G_CALLBACK (_basic_auth_cb), data);
+ return TRUE;
+}
+
+static gboolean
+_webflow_start (FlatpakTransaction *transaction,
+ const char *remote,
+ const char *url,
+ GVariant *options,
+ guint id,
+ GsPlugin *plugin)
+{
+ const char *browser;
+ g_autoptr(GError) error_local = NULL;
+
+ if (flatpak_transaction_get_no_interaction (transaction))
+ return FALSE;
+
+ g_debug ("Authentication required for remote '%s'", remote);
+
+ /* Allow hard overrides with $BROWSER */
+ browser = g_getenv ("BROWSER");
+ if (browser != NULL) {
+ const char *args[3] = { NULL, url, NULL };
+ args[0] = browser;
+ if (!g_spawn_async (NULL, (char **)args, NULL, G_SPAWN_SEARCH_PATH,
+ NULL, NULL, NULL, &error_local)) {
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ g_warning ("Failed to start browser %s: %s", browser, error_local->message);
+
+ gs_flatpak_error_convert (&error_local);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+
+ return FALSE;
+ }
+ } else {
+ if (!g_app_info_launch_default_for_uri (url, NULL, &error_local)) {
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ g_warning ("Failed to show url: %s", error_local->message);
+
+ gs_flatpak_error_convert (&error_local);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+
+ return FALSE;
+ }
+ }
+
+ g_debug ("Waiting for browser...");
+
+ return TRUE;
+}
+
+static void
+_webflow_done (FlatpakTransaction *transaction,
+ GVariant *options,
+ guint id,
+ GsPlugin *plugin)
+{
+ g_debug ("Browser done");
+}
+
+static FlatpakTransaction *
+_build_transaction (GsPlugin *plugin, GsFlatpak *flatpak,
+ gboolean interactive,
+ GCancellable *cancellable, GError **error)
+{
+ FlatpakInstallation *installation;
+ g_autoptr(FlatpakInstallation) installation_clone = NULL;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+
+ installation = gs_flatpak_get_installation (flatpak, interactive);
+
+ installation_clone = g_object_ref (installation);
+
+ /* create transaction */
+ transaction = gs_flatpak_transaction_new (installation_clone, cancellable, error);
+ if (transaction == NULL) {
+ g_prefix_error (error, "failed to build transaction: ");
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+
+ /* Let flatpak know if it is a background operation */
+ flatpak_transaction_set_no_interaction (transaction, !interactive);
+
+ /* connect up signals */
+ g_signal_connect (transaction, "ref-to-app",
+ G_CALLBACK (_ref_to_app), plugin);
+ g_signal_connect (transaction, "basic-auth-start",
+ G_CALLBACK (_basic_auth_start), plugin);
+ g_signal_connect (transaction, "webflow-start",
+ G_CALLBACK (_webflow_start), plugin);
+ g_signal_connect (transaction, "webflow-done",
+ G_CALLBACK (_webflow_done), plugin);
+
+ /* use system installations as dependency sources for user installations */
+ flatpak_transaction_add_default_dependency_sources (transaction);
+
+ return g_steal_pointer (&transaction);
+}
+
+static void
+remove_schedule_entry (gpointer schedule_entry_handle)
+{
+ g_autoptr(GError) error_local = NULL;
+
+ if (!gs_metered_remove_from_download_scheduler (schedule_entry_handle, NULL, &error_local))
+ g_warning ("Failed to remove schedule entry: %s", error_local->message);
+}
+
+gboolean
+gs_plugin_download (GsPlugin *plugin, GsAppList *list,
+ GCancellable *cancellable, GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GHashTable) applist_by_flatpaks = NULL;
+ GHashTableIter iter;
+ gpointer key, value;
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ /* build and run transaction for each flatpak installation */
+ applist_by_flatpaks = _group_apps_by_installation (self, list);
+ g_hash_table_iter_init (&iter, applist_by_flatpaks);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ GsFlatpak *flatpak = GS_FLATPAK (key);
+ GsAppList *list_tmp = GS_APP_LIST (value);
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ gpointer schedule_entry_handle = NULL;
+
+ g_assert (GS_IS_FLATPAK (flatpak));
+ g_assert (list_tmp != NULL);
+ g_assert (gs_app_list_length (list_tmp) > 0);
+
+ if (!interactive) {
+ g_autoptr(GError) error_local = NULL;
+
+ if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, &schedule_entry_handle, cancellable, &error_local)) {
+ g_warning ("Failed to block on download scheduler: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ /* build and run non-deployed transaction */
+ transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ flatpak_transaction_set_no_deploy (transaction, TRUE);
+
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ g_autofree gchar *ref = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ ref = gs_flatpak_app_get_ref_display (app);
+ if (flatpak_transaction_add_update (transaction, ref, NULL, NULL, &error_local))
+ continue;
+
+ /* Errors about missing remotes are not fatal, as that’s
+ * a not-uncommon situation. */
+ if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REMOTE_NOT_FOUND)) {
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ g_warning ("Skipping update for ‘%s’: %s", ref, error_local->message);
+
+ gs_flatpak_error_convert (&error_local);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+ } else {
+ gs_flatpak_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+
+ if (!gs_flatpak_transaction_run (transaction, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ remove_schedule_entry (schedule_entry_handle);
+ return FALSE;
+ }
+
+ remove_schedule_entry (schedule_entry_handle);
+
+ /* Traverse over the GsAppList again and set that the update has been already downloaded
+ * for the apps. */
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ gs_app_set_is_update_downloaded (app, TRUE);
+ }
+ }
+
+ return TRUE;
+}
+
+static void
+gs_flatpak_cover_addons_in_transaction (GsPlugin *plugin,
+ FlatpakTransaction *transaction,
+ GsApp *parent_app,
+ GsAppState state)
+{
+ g_autoptr(GsAppList) addons = NULL;
+ g_autoptr(GString) errors = NULL;
+ guint ii, sz;
+
+ g_return_if_fail (transaction != NULL);
+ g_return_if_fail (GS_IS_APP (parent_app));
+
+ addons = gs_app_dup_addons (parent_app);
+ sz = addons ? gs_app_list_length (addons) : 0;
+
+ for (ii = 0; ii < sz; ii++) {
+ GsApp *addon = gs_app_list_index (addons, ii);
+ g_autoptr(GError) local_error = NULL;
+
+ if (state == GS_APP_STATE_INSTALLING && gs_app_get_to_be_installed (addon)) {
+ g_autofree gchar *ref = NULL;
+
+ ref = gs_flatpak_app_get_ref_display (addon);
+ if (flatpak_transaction_add_install (transaction, gs_app_get_origin (addon), ref, NULL, &local_error)) {
+ gs_app_set_state (addon, state);
+ } else {
+ if (errors)
+ g_string_append_c (errors, '\n');
+ else
+ errors = g_string_new (NULL);
+ g_string_append_printf (errors, _("Failed to add to install for addon ‘%s’: %s"),
+ gs_app_get_name (addon), local_error->message);
+ }
+ } else if (state == GS_APP_STATE_REMOVING && gs_app_get_state (addon) == GS_APP_STATE_INSTALLED) {
+ g_autofree gchar *ref = NULL;
+
+ ref = gs_flatpak_app_get_ref_display (addon);
+ if (flatpak_transaction_add_uninstall (transaction, ref, &local_error)) {
+ gs_app_set_state (addon, state);
+ } else {
+ if (errors)
+ g_string_append_c (errors, '\n');
+ else
+ errors = g_string_new (NULL);
+ g_string_append_printf (errors, _("Failed to add to uninstall for addon ‘%s’: %s"),
+ gs_app_get_name (addon), local_error->message);
+ }
+ }
+ }
+
+ if (errors) {
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_autoptr(GError) error_local = g_error_new_literal (GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ errors->str);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+ }
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ GsFlatpak *flatpak;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ g_autofree gchar *ref = NULL;
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ /* not supported */
+ flatpak = gs_plugin_flatpak_get_handler (self, app);
+ if (flatpak == NULL)
+ return TRUE;
+
+ /* is a source, handled by dedicated function */
+ g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE);
+
+ /* build and run transaction */
+ transaction = _build_transaction (plugin, flatpak, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE), cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* add to the transaction cache for quick look up -- other unrelated
+ * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */
+ gs_flatpak_transaction_add_app (transaction, app);
+
+ ref = gs_flatpak_app_get_ref_display (app);
+ if (!flatpak_transaction_add_uninstall (transaction, ref, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ gs_flatpak_cover_addons_in_transaction (plugin, transaction, app, GS_APP_STATE_REMOVING);
+
+ /* run transaction */
+ gs_app_set_state (app, GS_APP_STATE_REMOVING);
+ if (!gs_flatpak_transaction_run (transaction, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ gs_app_set_state_recover (app);
+ return FALSE;
+ }
+
+ /* get any new state */
+ gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWN, 0);
+ gs_app_set_size_installed (app, GS_SIZE_TYPE_UNKNOWN, 0);
+
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_flatpak_refine_app (flatpak, app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ interactive,
+ cancellable, error)) {
+ g_prefix_error (error, "failed to run refine for %s: ", ref);
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ gs_flatpak_refine_addons (flatpak,
+ app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ GS_APP_STATE_REMOVING,
+ interactive,
+ cancellable);
+
+ return TRUE;
+}
+
+static gboolean
+app_has_local_source (GsApp *app)
+{
+ const gchar *url = gs_app_get_origin_hostname (app);
+
+ if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE)
+ return TRUE;
+
+ if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF &&
+ g_strcmp0 (url, "localhost") == 0)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+gs_plugin_flatpak_ensure_scope (GsPlugin *plugin,
+ GsApp *app)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+
+ if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) {
+ g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software");
+
+ /* get the new GsFlatpak for handling of local files */
+ gs_app_set_scope (app, g_settings_get_boolean (settings, "install-bundles-system-wide") ?
+ AS_COMPONENT_SCOPE_SYSTEM : AS_COMPONENT_SCOPE_USER);
+ if (!self->has_system_helper) {
+ g_info ("no flatpak system helper is available, using user");
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER);
+ }
+ if (self->destdir_for_tests != NULL) {
+ g_debug ("in self tests, using user");
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER);
+ }
+ }
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ GsFlatpak *flatpak;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ g_autoptr(GError) error_local = NULL;
+ gpointer schedule_entry_handle = NULL;
+ gboolean already_installed = FALSE;
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ /* queue for install if installation needs the network */
+ if (!app_has_local_source (app) &&
+ !gs_plugin_get_network_available (plugin)) {
+ gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL);
+ return TRUE;
+ }
+
+ /* set the app scope */
+ gs_plugin_flatpak_ensure_scope (plugin, app);
+
+ /* not supported */
+ flatpak = gs_plugin_flatpak_get_handler (self, app);
+ if (flatpak == NULL)
+ return TRUE;
+
+ /* is a source, handled by dedicated function */
+ g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE);
+
+ /* build */
+ transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* add to the transaction cache for quick look up -- other unrelated
+ * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */
+ gs_flatpak_transaction_add_app (transaction, app);
+
+ /* add flatpakref */
+ if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF) {
+ GFile *file = gs_app_get_local_file (app);
+ g_autoptr(GBytes) blob = NULL;
+ if (file == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no local file set for bundle %s",
+ gs_app_get_unique_id (app));
+ return FALSE;
+ }
+ blob = g_file_load_bytes (file, cancellable, NULL, error);
+ if (blob == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!flatpak_transaction_add_install_flatpakref (transaction, blob, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* add bundle */
+ } else if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE) {
+ GFile *file = gs_app_get_local_file (app);
+ if (file == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no local file set for bundle %s",
+ gs_app_get_unique_id (app));
+ return FALSE;
+ }
+ if (!flatpak_transaction_add_install_bundle (transaction, file,
+ NULL, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* add normal ref */
+ } else {
+ g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app);
+ if (!flatpak_transaction_add_install (transaction,
+ gs_app_get_origin (app),
+ ref, NULL, &error_local)) {
+ /* Somehow, the app might already be installed. */
+ if (g_error_matches (error_local, FLATPAK_ERROR,
+ FLATPAK_ERROR_ALREADY_INSTALLED)) {
+ already_installed = TRUE;
+ g_clear_error (&error_local);
+ } else {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+ }
+
+ gs_flatpak_cover_addons_in_transaction (plugin, transaction, app, GS_APP_STATE_INSTALLING);
+
+ if (!interactive) {
+ /* FIXME: Add additional details here, especially the download
+ * size bounds (using `size-minimum` and `size-maximum`, both
+ * type `t`). */
+ if (!gs_metered_block_app_on_download_scheduler (app, &schedule_entry_handle, cancellable, &error_local)) {
+ g_warning ("Failed to block on download scheduler: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ /* run transaction */
+ if (!already_installed) {
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ if (!gs_flatpak_transaction_run (transaction, cancellable, &error_local)) {
+ /* Somehow, the app might already be installed. */
+ if (g_error_matches (error_local, FLATPAK_ERROR,
+ FLATPAK_ERROR_ALREADY_INSTALLED)) {
+ already_installed = TRUE;
+ g_clear_error (&error_local);
+ } else {
+ if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REF_NOT_FOUND)) {
+ const gchar *origin = gs_app_get_origin (app);
+ if (origin != NULL) {
+ g_autoptr(FlatpakRemote) remote = NULL;
+ remote = flatpak_installation_get_remote_by_name (gs_flatpak_get_installation (flatpak, interactive),
+ origin, cancellable, NULL);
+ if (remote != NULL) {
+ g_autofree gchar *filter = flatpak_remote_get_filter (remote);
+ if (filter != NULL && *filter != '\0') {
+ /* It's a filtered remote, create a user friendly error message for it */
+ g_autoptr(GError) error_tmp = NULL;
+ g_set_error (&error_tmp, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ _("Remote “%s” doesn't allow install of “%s”, possibly due to its filter. Remove the filter and repeat the install. Detailed error: %s"),
+ flatpak_remote_get_title (remote),
+ gs_app_get_name (app),
+ error_local->message);
+ g_clear_error (&error_local);
+ error_local = g_steal_pointer (&error_tmp);
+ }
+ }
+ }
+ }
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_flatpak_error_convert (error);
+ gs_app_set_state_recover (app);
+ remove_schedule_entry (schedule_entry_handle);
+ return FALSE;
+ }
+ }
+ }
+
+ if (already_installed) {
+ /* Set the app back to UNKNOWN so that refining it gets all the right details. */
+ g_debug ("App %s is already installed", gs_app_get_unique_id (app));
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ }
+
+ remove_schedule_entry (schedule_entry_handle);
+
+ /* get any new state */
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_flatpak_refine_app (flatpak, app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ interactive,
+ cancellable, error)) {
+ g_prefix_error (error, "failed to run refine for %s: ",
+ gs_app_get_unique_id (app));
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ gs_flatpak_refine_addons (flatpak,
+ app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ GS_APP_STATE_INSTALLING,
+ interactive,
+ cancellable);
+
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_flatpak_update (GsPlugin *plugin,
+ GsFlatpak *flatpak,
+ GsAppList *list_tmp,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ gboolean is_update_downloaded = TRUE;
+ gpointer schedule_entry_handle = NULL;
+
+ if (!interactive) {
+ g_autoptr(GError) error_local = NULL;
+
+ if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, &schedule_entry_handle, cancellable, &error_local)) {
+ g_warning ("Failed to block on download scheduler: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ /* build and run transaction */
+ transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ g_autofree gchar *ref = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ ref = gs_flatpak_app_get_ref_display (app);
+ if (flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) {
+ /* add to the transaction cache for quick look up -- other unrelated
+ * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */
+ gs_flatpak_transaction_add_app (transaction, app);
+
+ continue;
+ }
+
+ /* Errors about missing remotes are not fatal, as that’s
+ * a not-uncommon situation. */
+ if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REMOTE_NOT_FOUND)) {
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ g_warning ("Skipping update for ‘%s’: %s", ref, error_local->message);
+
+ gs_flatpak_error_convert (&error_local);
+
+ event = gs_plugin_event_new ("error", error_local,
+ NULL);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+ } else {
+ gs_flatpak_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+
+ /* run transaction */
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+
+ /* If all apps' update are previously downloaded and available locally,
+ * FlatpakTransaction should run with no-pull flag. This is the case
+ * for apps' autoupdates. */
+ is_update_downloaded &= gs_app_get_is_update_downloaded (app);
+ }
+
+ if (is_update_downloaded) {
+ flatpak_transaction_set_no_pull (transaction, TRUE);
+ }
+
+ /* automatically clean up unused EOL runtimes when updating */
+ flatpak_transaction_set_include_unused_uninstall_ops (transaction, TRUE);
+
+ if (!gs_flatpak_transaction_run (transaction, cancellable, error)) {
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ gs_app_set_state_recover (app);
+ }
+ gs_flatpak_error_convert (error);
+ remove_schedule_entry (schedule_entry_handle);
+ return FALSE;
+ } else {
+ /* Reset the state to have it updated */
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ }
+ }
+
+ remove_schedule_entry (schedule_entry_handle);
+ gs_plugin_updates_changed (plugin);
+
+ /* get any new state */
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < gs_app_list_length (list_tmp); i++) {
+ GsApp *app = gs_app_list_index (list_tmp, i);
+ g_autofree gchar *ref = NULL;
+
+ ref = gs_flatpak_app_get_ref_display (app);
+ if (!gs_flatpak_refine_app (flatpak, app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ interactive,
+ cancellable, error)) {
+ g_prefix_error (error, "failed to run refine for %s: ", ref);
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_update (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GHashTable) applist_by_flatpaks = NULL;
+ GHashTableIter iter;
+ gpointer key, value;
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ /* build and run transaction for each flatpak installation */
+ applist_by_flatpaks = _group_apps_by_installation (self, list);
+ g_hash_table_iter_init (&iter, applist_by_flatpaks);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ GsFlatpak *flatpak = GS_FLATPAK (key);
+ GsAppList *list_tmp = GS_APP_LIST (value);
+ gboolean success;
+
+ g_assert (GS_IS_FLATPAK (flatpak));
+ g_assert (list_tmp != NULL);
+ g_assert (gs_app_list_length (list_tmp) > 0);
+
+ gs_flatpak_set_busy (flatpak, TRUE);
+ success = gs_plugin_flatpak_update (plugin, flatpak, list_tmp, interactive, cancellable, error);
+ gs_flatpak_set_busy (flatpak, FALSE);
+ if (!success)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_repo (GsPluginFlatpak *self,
+ GFile *file,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsApp) app = NULL;
+
+ /* parse the repo file */
+ app = gs_flatpak_app_new_from_repo_file (file, cancellable, error);
+ if (app == NULL)
+ return NULL;
+
+ /* already exists */
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsApp) app_tmp = NULL;
+ app_tmp = gs_flatpak_find_source_by_url (flatpak,
+ gs_flatpak_app_get_repo_url (app),
+ interactive,
+ cancellable, &error_local);
+ if (app_tmp == NULL) {
+ g_debug ("%s", error_local->message);
+ continue;
+ }
+ if (g_strcmp0 (gs_flatpak_app_get_repo_filter (app), gs_flatpak_app_get_repo_filter (app_tmp)) != 0)
+ continue;
+ return g_steal_pointer (&app_tmp);
+ }
+
+ /* this is new */
+ gs_app_set_management_plugin (app, GS_PLUGIN (self));
+ return g_steal_pointer (&app);
+}
+
+static GsFlatpak *
+gs_plugin_flatpak_create_temporary (GsPluginFlatpak *self,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *installation_path = NULL;
+ g_autoptr(FlatpakInstallation) installation = NULL;
+ g_autoptr(GFile) installation_file = NULL;
+
+ /* create new per-user installation in a cache dir */
+ installation_path = gs_utils_get_cache_filename ("flatpak",
+ "installation-tmp",
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_ENSURE_EMPTY |
+ GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
+ error);
+ if (installation_path == NULL)
+ return NULL;
+ installation_file = g_file_new_for_path (installation_path);
+ installation = flatpak_installation_new_for_path (installation_file,
+ TRUE, /* user */
+ cancellable,
+ error);
+ if (installation == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ return gs_flatpak_new (GS_PLUGIN (self), installation, GS_FLATPAK_FLAG_IS_TEMPORARY);
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_bundle (GsPluginFlatpak *self,
+ GFile *file,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *ref = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app_tmp = NULL;
+ g_autoptr(GsFlatpak) flatpak_tmp = NULL;
+
+ /* only use the temporary GsFlatpak to avoid the auth dialog */
+ flatpak_tmp = gs_plugin_flatpak_create_temporary (self, cancellable, error);
+ if (flatpak_tmp == NULL)
+ return NULL;
+
+ /* First make a quick GsApp to get the ref */
+ app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, TRUE /* unrefined */,
+ interactive, cancellable, error);
+ if (app == NULL)
+ return NULL;
+
+ /* is this already installed or available in a configured remote */
+ ref = gs_flatpak_app_get_ref_display (app);
+ app_tmp = gs_plugin_flatpak_find_app_by_ref (self, ref, interactive, cancellable, NULL);
+ if (app_tmp != NULL)
+ return g_steal_pointer (&app_tmp);
+
+ /* If not installed/available, make a fully refined GsApp */
+ g_clear_object (&app);
+ app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, FALSE /* unrefined */,
+ interactive, cancellable, error);
+ if (app == NULL)
+ return NULL;
+
+ /* force this to be 'any' scope for installation */
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN);
+
+ /* this is new */
+ return g_steal_pointer (&app);
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_ref (GsPluginFlatpak *self,
+ GFile *file,
+ gboolean interactive,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsApp *runtime;
+ g_autofree gchar *ref = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app_tmp = NULL;
+ g_autoptr(GsFlatpak) flatpak_tmp = NULL;
+
+ /* only use the temporary GsFlatpak to avoid the auth dialog */
+ flatpak_tmp = gs_plugin_flatpak_create_temporary (self, cancellable, error);
+ if (flatpak_tmp == NULL)
+ return NULL;
+
+ /* First make a quick GsApp to get the ref */
+ app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, TRUE /* unrefined */,
+ interactive, cancellable, error);
+ if (app == NULL)
+ return NULL;
+
+ /* is this already installed or available in a configured remote */
+ ref = gs_flatpak_app_get_ref_display (app);
+ app_tmp = gs_plugin_flatpak_find_app_by_ref (self, ref, interactive, cancellable, NULL);
+ if (app_tmp != NULL)
+ return g_steal_pointer (&app_tmp);
+
+ /* If not installed/available, make a fully refined GsApp */
+ g_clear_object (&app);
+ app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, FALSE /* unrefined */,
+ interactive, cancellable, error);
+ if (app == NULL)
+ return NULL;
+
+ /* force this to be 'any' scope for installation */
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN);
+
+ /* do we have a system runtime available */
+ runtime = gs_app_get_runtime (app);
+ if (runtime != NULL) {
+ g_autoptr(GsApp) runtime_tmp = NULL;
+ g_autofree gchar *runtime_ref = gs_flatpak_app_get_ref_display (runtime);
+ runtime_tmp = gs_plugin_flatpak_find_app_by_ref (self,
+ runtime_ref,
+ interactive,
+ cancellable,
+ NULL);
+ if (runtime_tmp != NULL) {
+ gs_app_set_runtime (app, runtime_tmp);
+ } else {
+ /* the new runtime is available from the RuntimeRepo */
+ if (gs_flatpak_app_get_runtime_url (runtime) != NULL)
+ gs_app_set_state (runtime, GS_APP_STATE_AVAILABLE);
+ }
+ }
+
+ /* this is new */
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_plugin_file_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autofree gchar *content_type = NULL;
+ g_autoptr(GsApp) app = NULL;
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+ const gchar *mimetypes_bundle[] = {
+ "application/vnd.flatpak",
+ NULL };
+ const gchar *mimetypes_repo[] = {
+ "application/vnd.flatpak.repo",
+ NULL };
+ const gchar *mimetypes_ref[] = {
+ "application/vnd.flatpak.ref",
+ NULL };
+
+ /* does this match any of the mimetypes we support */
+ content_type = gs_utils_get_content_type (file, cancellable, error);
+ if (content_type == NULL)
+ return FALSE;
+ if (g_strv_contains (mimetypes_bundle, content_type)) {
+ app = gs_plugin_flatpak_file_to_app_bundle (self, file, interactive,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ } else if (g_strv_contains (mimetypes_repo, content_type)) {
+ app = gs_plugin_flatpak_file_to_app_repo (self, file, interactive,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ } else if (g_strv_contains (mimetypes_ref, content_type)) {
+ app = gs_plugin_flatpak_file_to_app_ref (self, file, interactive,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ }
+ if (app != NULL) {
+ GsApp *runtime = gs_app_get_runtime (app);
+ /* Ensure the origin for the runtime is set */
+ if (runtime != NULL && gs_app_get_origin (runtime) == NULL) {
+ g_autoptr(GError) error_local = NULL;
+ if (!gs_plugin_flatpak_refine_app (self, runtime, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN, interactive, cancellable, &error_local))
+ g_debug ("Failed to refine runtime: %s", error_local->message);
+ }
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+static void refine_categories_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_refine_categories_async (GsPlugin *plugin,
+ GPtrArray *list,
+ GsPluginRefineCategoriesFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (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_flatpak_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)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ GsPluginRefineCategoriesData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+
+ if (!gs_flatpak_refine_category_sizes (flatpak, data->list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_flatpak_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_flatpak_list_apps_async (GsPlugin *plugin,
+ GsAppQuery *query,
+ GsPluginListAppsFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (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_flatpak_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)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ GsPluginListAppsData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
+ 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;
+ const gchar *provides_tag = NULL;
+ GsAppQueryProvidesType provides_type = GS_APP_QUERY_PROVIDES_UNKNOWN;
+ 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);
+ provides_type = gs_app_query_get_provides (data->query, &provides_tag);
+ }
+
+ if (released_since != NULL) {
+ g_autoptr(GDateTime) now = g_date_time_new_now_local ();
+ 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 &&
+ provides_tag == 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;
+ }
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ const gchar * const provides_tag_strv[2] = { provides_tag, NULL };
+
+ if (released_since != NULL &&
+ !gs_flatpak_add_recent (flatpak, list, age_secs, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_curated != GS_APP_QUERY_TRISTATE_UNSET &&
+ !gs_flatpak_add_popular (flatpak, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_featured != GS_APP_QUERY_TRISTATE_UNSET &&
+ !gs_flatpak_add_featured (flatpak, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (category != NULL &&
+ !gs_flatpak_add_category_apps (flatpak, category, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (is_installed != GS_APP_QUERY_TRISTATE_UNSET &&
+ !gs_flatpak_add_installed (flatpak, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (deployment_featured != NULL &&
+ !gs_flatpak_add_deployment_featured (flatpak, list, interactive, deployment_featured, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (developers != NULL &&
+ !gs_flatpak_search_developer_apps (flatpak, developers, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (keywords != NULL &&
+ !gs_flatpak_search (flatpak, keywords, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (alternate_of != NULL &&
+ !gs_flatpak_add_alternates (flatpak, alternate_of, list, interactive, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* The @provides_type is deliberately ignored here, as flatpak
+ * wants to try and match anything. This could be changed in
+ * future. */
+ if (provides_tag != NULL &&
+ provides_type != GS_APP_QUERY_PROVIDES_UNKNOWN &&
+ !gs_flatpak_search (flatpak, provides_tag_strv, list, interactive, 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_flatpak_list_apps_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+gboolean
+gs_plugin_url_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+ for (guint i = 0; i < self->installations->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+ if (!gs_flatpak_url_to_app (flatpak, list, url, interactive, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void install_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_install_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_install_repository_async);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* is a source */
+ g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY);
+
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ install_repository_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+install_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsFlatpak *flatpak;
+ GsPluginManageRepositoryData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ /* queue for install if installation needs the network */
+ if (!app_has_local_source (data->repository) &&
+ !gs_plugin_get_network_available (GS_PLUGIN (self))) {
+ gs_app_set_state (data->repository, GS_APP_STATE_QUEUED_FOR_INSTALL);
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ gs_plugin_flatpak_ensure_scope (GS_PLUGIN (self), data->repository);
+
+ flatpak = gs_plugin_flatpak_get_handler (self, data->repository);
+ if (flatpak == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ if (gs_flatpak_app_install_source (flatpak, data->repository, TRUE, interactive, cancellable, &local_error))
+ g_task_return_boolean (task, TRUE);
+ else
+ g_task_return_error (task, g_steal_pointer (&local_error));
+}
+
+static gboolean
+gs_plugin_flatpak_install_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void remove_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_remove_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_remove_repository_async);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* is a source */
+ g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY);
+
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ remove_repository_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+remove_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsFlatpak *flatpak;
+ GsPluginManageRepositoryData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ flatpak = gs_plugin_flatpak_get_handler (self, data->repository);
+ if (flatpak == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ if (gs_flatpak_app_remove_source (flatpak, data->repository, TRUE, interactive, cancellable, &local_error))
+ g_task_return_boolean (task, TRUE);
+ else
+ g_task_return_error (task, g_steal_pointer (&local_error));
+}
+
+static gboolean
+gs_plugin_flatpak_remove_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void enable_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_enable_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_enable_repository_async);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* is a source */
+ g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY);
+
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ enable_repository_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+enable_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsFlatpak *flatpak;
+ GsPluginManageRepositoryData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ flatpak = gs_plugin_flatpak_get_handler (self, data->repository);
+ if (flatpak == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ if (gs_flatpak_app_install_source (flatpak, data->repository, FALSE, interactive, cancellable, &local_error))
+ g_task_return_boolean (task, TRUE);
+ else
+ g_task_return_error (task, g_steal_pointer (&local_error));
+}
+
+static gboolean
+gs_plugin_flatpak_enable_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void disable_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable);
+
+static void
+gs_plugin_flatpak_disable_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+
+ task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_flatpak_disable_repository_async);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* is a source */
+ g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY);
+
+ gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
+ disable_repository_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker. */
+static void
+disable_repository_thread_cb (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object);
+ GsFlatpak *flatpak;
+ GsPluginManageRepositoryData *data = task_data;
+ gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE);
+ g_autoptr(GError) local_error = NULL;
+
+ assert_in_worker (self);
+
+ flatpak = gs_plugin_flatpak_get_handler (self, data->repository);
+ if (flatpak == NULL) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ if (gs_flatpak_app_remove_source (flatpak, data->repository, FALSE, interactive, cancellable, &local_error))
+ g_task_return_boolean (task, TRUE);
+ else
+ g_task_return_error (task, g_steal_pointer (&local_error));
+}
+
+static gboolean
+gs_plugin_flatpak_disable_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_flatpak_class_init (GsPluginFlatpakClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_flatpak_dispose;
+
+ plugin_class->setup_async = gs_plugin_flatpak_setup_async;
+ plugin_class->setup_finish = gs_plugin_flatpak_setup_finish;
+ plugin_class->shutdown_async = gs_plugin_flatpak_shutdown_async;
+ plugin_class->shutdown_finish = gs_plugin_flatpak_shutdown_finish;
+ plugin_class->refine_async = gs_plugin_flatpak_refine_async;
+ plugin_class->refine_finish = gs_plugin_flatpak_refine_finish;
+ plugin_class->list_apps_async = gs_plugin_flatpak_list_apps_async;
+ plugin_class->list_apps_finish = gs_plugin_flatpak_list_apps_finish;
+ plugin_class->refresh_metadata_async = gs_plugin_flatpak_refresh_metadata_async;
+ plugin_class->refresh_metadata_finish = gs_plugin_flatpak_refresh_metadata_finish;
+ plugin_class->install_repository_async = gs_plugin_flatpak_install_repository_async;
+ plugin_class->install_repository_finish = gs_plugin_flatpak_install_repository_finish;
+ plugin_class->remove_repository_async = gs_plugin_flatpak_remove_repository_async;
+ plugin_class->remove_repository_finish = gs_plugin_flatpak_remove_repository_finish;
+ plugin_class->enable_repository_async = gs_plugin_flatpak_enable_repository_async;
+ plugin_class->enable_repository_finish = gs_plugin_flatpak_enable_repository_finish;
+ plugin_class->disable_repository_async = gs_plugin_flatpak_disable_repository_async;
+ plugin_class->disable_repository_finish = gs_plugin_flatpak_disable_repository_finish;
+ plugin_class->refine_categories_async = gs_plugin_flatpak_refine_categories_async;
+ plugin_class->refine_categories_finish = gs_plugin_flatpak_refine_categories_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_FLATPAK;
+}
diff --git a/plugins/flatpak/gs-plugin-flatpak.h b/plugins/flatpak/gs-plugin-flatpak.h
new file mode 100644
index 0000000..8426156
--- /dev/null
+++ b/plugins/flatpak/gs-plugin-flatpak.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_FLATPAK (gs_plugin_flatpak_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginFlatpak, gs_plugin_flatpak, GS, PLUGIN_FLATPAK, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-self-test.c b/plugins/flatpak/gs-self-test.c
new file mode 100644
index 0000000..6f4bd7f
--- /dev/null
+++ b/plugins/flatpak/gs-self-test.c
@@ -0,0 +1,2003 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2017 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+
+#include "gnome-software-private.h"
+
+#include "gs-flatpak-app.h"
+
+#include "gs-test.h"
+
+const gchar * const allowlist[] = {
+ "appstream",
+ "flatpak",
+ "icons",
+ NULL
+};
+
+static gboolean
+gs_flatpak_test_write_repo_file (const gchar *fn, const gchar *testdir, GFile **file_out, GError **error)
+{
+ g_autofree gchar *testdir_repourl = NULL;
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *path = NULL;
+
+ /* create file */
+ testdir_repourl = g_strdup_printf ("file://%s/repo", testdir);
+ g_string_append (str, "[Flatpak Repo]\n");
+ g_string_append (str, "Title=foo-bar\n");
+ g_string_append (str, "Comment=Longer one line comment\n");
+ g_string_append (str, "Description=Longer multiline comment that "
+ "does into detail.\n");
+ g_string_append (str, "DefaultBranch=master\n");
+ g_string_append_printf (str, "Url=%s\n", testdir_repourl);
+ g_string_append (str, "Homepage=http://foo.bar\n");
+
+ path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), fn, NULL);
+ *file_out = g_file_new_for_path (path);
+
+ return g_file_set_contents (path, str->str, -1, error);
+}
+
+static gboolean
+gs_flatpak_test_write_ref_file (const gchar *filename, const gchar *url, const gchar *runtimerepo, GFile **file_out, GError **error)
+{
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *path = NULL;
+
+ g_return_val_if_fail (filename != NULL, FALSE);
+ g_return_val_if_fail (url != NULL, FALSE);
+ g_return_val_if_fail (runtimerepo != NULL, FALSE);
+
+ g_string_append (str, "[Flatpak Ref]\n");
+ g_string_append (str, "Title=Chiron\n");
+ g_string_append (str, "Name=org.test.Chiron\n");
+ g_string_append (str, "Branch=master\n");
+ g_string_append_printf (str, "Url=%s\n", url);
+ g_string_append (str, "IsRuntime=false\n");
+ g_string_append (str, "Comment=Single line synopsis\n");
+ g_string_append (str, "Description=A Testing Application\n");
+ g_string_append (str, "Icon=https://getfedora.org/static/images/fedora-logotext.png\n");
+ g_string_append_printf (str, "RuntimeRepo=%s\n", runtimerepo);
+
+ path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), filename, NULL);
+ *file_out = g_file_new_for_path (path);
+
+ return g_file_set_contents (path, str->str, -1, error);
+}
+
+/* create duplicate file as if downloaded in firefox */
+static void
+gs_plugins_flatpak_repo_non_ascii_func (GsPluginLoader *plugin_loader)
+{
+ const gchar *fn = "example (1)….flatpakrepo";
+ gboolean ret;
+ g_autofree gchar *testdir = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* get a resolvable */
+ testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime");
+ if (testdir == NULL)
+ return;
+
+ ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpstr (gs_app_get_unique_id (app), ==, "*/*/*/example__1____/master");
+}
+
+static void
+gs_plugins_flatpak_repo_func (GsPluginLoader *plugin_loader)
+{
+ const gchar *group_name = "remote \"example\"";
+ const gchar *root = NULL;
+ const gchar *fn = "example.flatpakrepo";
+ gboolean ret;
+ g_autofree gchar *config_fn = NULL;
+ g_autofree gchar *remote_url = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autofree gchar *testdir_repourl = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(GsApp) app2 = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GIcon) icon = NULL;
+ g_autoptr(GsPlugin) management_plugin = NULL;
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* get a resolvable */
+ testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime");
+ if (testdir == NULL)
+ return;
+ testdir_repourl = g_strdup_printf ("file://%s/repo", testdir);
+
+ /* create file */
+ ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* load local file */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_REPOSITORY);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "example");
+ management_plugin = gs_app_dup_management_plugin (app);
+ g_assert_nonnull (management_plugin);
+ g_assert_cmpstr (gs_plugin_get_name (management_plugin), ==, "flatpak");
+ g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost");
+ g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://foo.bar");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "foo-bar");
+ g_assert_cmpstr (gs_app_get_summary (app), ==, "Longer one line comment");
+ g_assert_cmpstr (gs_app_get_description (app), ==,
+ "Longer multiline comment that does into detail.");
+ g_assert_true (gs_app_get_local_file (app) != NULL);
+ icon = gs_app_get_icon_for_size (app, 64, 1, NULL);
+ g_assert_nonnull (icon);
+
+ /* now install the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+
+ /* check config file was updated */
+ root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR");
+ config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL);
+ kf = g_key_file_new ();
+ ret = g_key_file_load_from_file (kf, config_fn, 0, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ g_assert_true (g_key_file_has_group (kf, "core"));
+ g_assert_true (g_key_file_has_group (kf, group_name));
+ g_assert_true (!g_key_file_get_boolean (kf, group_name, "gpg-verify", NULL));
+
+ /* check the URL was unmangled */
+ remote_url = g_key_file_get_string (kf, group_name, "url", &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (remote_url, ==, testdir_repourl);
+
+ /* try again, check state is correct */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ NULL);
+ app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app2 != NULL);
+ g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_INSTALLED);
+
+ /* disable repo */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+
+ /* enable repo */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+
+ /* remove it */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNAVAILABLE);
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+}
+
+static void
+progress_notify_cb (GObject *obj, GParamSpec *pspec, gpointer user_data)
+{
+ gboolean *seen_unknown = user_data;
+ GsApp *app = GS_APP (obj);
+
+ if (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN)
+ *seen_unknown = TRUE;
+}
+
+static void
+gs_plugins_flatpak_app_with_runtime_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app;
+ GsApp *runtime;
+ const gchar *root;
+ gboolean ret;
+ gint kf_remote_repo_version;
+ g_autofree gchar *changed_fn = NULL;
+ g_autofree gchar *config_fn = NULL;
+ g_autofree gchar *desktop_fn = NULL;
+ g_autofree gchar *kf_remote_url = NULL;
+ g_autofree gchar *metadata_fn = NULL;
+ g_autofree gchar *repodir_fn = NULL;
+ g_autofree gchar *runtime_fn = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autofree gchar *testdir_repourl = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GKeyFile) kf1 = g_key_file_new ();
+ g_autoptr(GKeyFile) kf2 = g_key_file_new ();
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsAppList) list_all = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) sources = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ gulong signal_id;
+ gboolean seen_unknown;
+ GsPlugin *plugin;
+ 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);
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* no files to use */
+ repodir_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo");
+ if (repodir_fn == NULL ||
+ !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+
+ /* check changed file exists */
+ root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR");
+ changed_fn = g_build_filename (root, "flatpak", ".changed", NULL);
+ g_assert_true (g_file_test (changed_fn, G_FILE_TEST_IS_REGULAR));
+
+ /* check repo is set up */
+ config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL);
+ ret = g_key_file_load_from_file (kf1, config_fn, G_KEY_FILE_NONE, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ kf_remote_repo_version = g_key_file_get_integer (kf1, "core", "repo_version", &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (kf_remote_repo_version, ==, 1);
+
+ /* add a remote */
+ app_source = gs_flatpak_app_new ("test");
+ testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime");
+ if (testdir == NULL)
+ return;
+ testdir_repourl = g_strdup_printf ("file://%s/repo", testdir);
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* check remote was set up */
+ ret = g_key_file_load_from_file (kf2, config_fn, G_KEY_FILE_NONE, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ kf_remote_url = g_key_file_get_string (kf2, "remote \"test\"", "url", &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (kf_remote_url, !=, NULL);
+
+ /* check the source now exists */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources != NULL);
+ g_assert_cmpint (gs_app_list_length (sources), ==, 1);
+ app = gs_app_list_index (sources, 0);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "test");
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_REPOSITORY);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* all the apps should have the flatpak keyword */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "flatpak";
+ 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);
+ g_clear_object (&query);
+
+ list_all = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (list_all != NULL);
+ g_assert_cmpint (gs_app_list_length (list_all), ==, 2);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "Bingo";
+ query = gs_app_query_new ("keywords", keywords,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME |
+ 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);
+ g_clear_object (&query);
+
+ list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (list != NULL);
+
+ /* make sure there is one entry, the flatpak 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), ==, "org.test.Chiron");
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpint ((gint64) gs_app_get_kudos (app), ==,
+ GS_APP_KUDO_MY_LANGUAGE |
+ GS_APP_KUDO_HAS_KEYWORDS |
+ GS_APP_KUDO_HI_DPI_ICON |
+ GS_APP_KUDO_SANDBOXED_SECURE |
+ GS_APP_KUDO_SANDBOXED);
+ g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL);
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL);
+ g_assert_cmpint (gs_app_get_update_urgency (app), ==, AS_URGENCY_KIND_UNKNOWN);
+
+ /* check runtime */
+ runtime = gs_app_get_runtime (app);
+ g_assert_true (runtime != NULL);
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* install, also installing runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN ||
+ gs_app_get_progress (app) == 100);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+
+ /* check the application exists in the right places */
+ metadata_fn = g_build_filename (root,
+ "flatpak",
+ "app",
+ "org.test.Chiron",
+ "current",
+ "active",
+ "metadata",
+ NULL);
+ g_assert_true (g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR));
+ desktop_fn = g_build_filename (root,
+ "flatpak",
+ "app",
+ "org.test.Chiron",
+ "current",
+ "active",
+ "export",
+ "share",
+ "applications",
+ "org.test.Chiron.desktop",
+ NULL);
+ g_assert_true (g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR));
+
+ /* check the runtime was installed as well */
+ runtime_fn = g_build_filename (root,
+ "flatpak",
+ "runtime",
+ "org.test.Runtime",
+ "x86_64",
+ "master",
+ "active",
+ "files",
+ "share",
+ "libtest",
+ "README",
+ NULL);
+ g_assert_true (g_file_test (runtime_fn, G_FILE_TEST_IS_REGULAR));
+
+ /* remove the application */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+ g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR));
+ g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR));
+
+ /* install again, to check whether the progress gets initialized;
+ * since installation happens in another thread, we have to monitor all
+ * changes to the progress and see if we see the one we want */
+ seen_unknown = (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN);
+ signal_id = g_signal_connect (app, "notify::progress",
+ G_CALLBACK (progress_notify_cb), &seen_unknown);
+
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+
+ /* progress should be set to unknown right before installing */
+ while (!seen_unknown)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_true (seen_unknown);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN ||
+ gs_app_get_progress (app) == 100);
+ g_signal_handler_disconnect (app, signal_id);
+
+ /* remove the application */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+ g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR));
+ g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR));
+
+ /* remove the remote (fail, as the runtime is still installed) */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED);
+ g_assert_true (!ret);
+ g_clear_error (&error);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* remove the runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+}
+
+static void
+gs_plugins_flatpak_app_missing_runtime_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app;
+ gboolean ret;
+ g_autofree gchar *repodir_fn = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autofree gchar *testdir_repourl = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsPlugin *plugin;
+ 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);
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* no files to use */
+ repodir_fn = gs_test_get_filename (TESTDATADIR, "app-missing-runtime/repo");
+ if (repodir_fn == NULL ||
+ !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+
+ /* add a remote */
+ app_source = gs_flatpak_app_new ("test");
+ testdir = gs_test_get_filename (TESTDATADIR, "app-missing-runtime");
+ if (testdir == NULL)
+ return;
+ testdir_repourl = g_strdup_printf ("file://%s/repo", testdir);
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "Bingo";
+ 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);
+ g_clear_object (&query);
+
+ list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (list != NULL);
+
+ /* make sure there is one entry, the flatpak 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), ==, "org.test.Chiron");
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+
+ /* install, also installing runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED);
+ g_assert_true (!ret);
+ g_clear_error (&error);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+}
+
+static void
+update_app_progress_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data)
+{
+ g_debug ("progress now %u%%", gs_app_get_progress (app));
+ if (user_data != NULL) {
+ guint *tmp = (guint *) user_data;
+ (*tmp)++;
+ }
+}
+
+static void
+update_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data)
+{
+ GsAppState state = gs_app_get_state (app);
+ g_debug ("state now %s", gs_app_state_to_string (state));
+ if (state == GS_APP_STATE_INSTALLING) {
+ gboolean *tmp = (gboolean *) user_data;
+ *tmp = TRUE;
+ }
+}
+
+static gboolean
+update_app_action_delay_cb (gpointer user_data)
+{
+ GMainLoop *loop = (GMainLoop *) user_data;
+ g_main_loop_quit (loop);
+ return FALSE;
+}
+
+static void
+update_app_action_finish_sync (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ gboolean ret;
+ g_autoptr(GError) error = NULL;
+ ret = gs_plugin_loader_job_action_finish (plugin_loader, res, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_timeout_add_seconds (5, update_app_action_delay_cb, user_data);
+}
+
+static void
+gs_plugins_flatpak_runtime_repo_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app_source;
+ GsApp *runtime;
+ const gchar *fn_ref = "test.flatpakref";
+ const gchar *fn_repo = "test.flatpakrepo";
+ gboolean ret;
+ g_autoptr(GFile) fn_repo_file = NULL;
+ g_autofree gchar *fn_repourl = NULL;
+ g_autofree gchar *testdir2 = NULL;
+ g_autofree gchar *testdir2_repourl = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) sources2 = NULL;
+ g_autoptr(GsAppList) sources = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* write a flatpakrepo file */
+ testdir = gs_test_get_filename (TESTDATADIR, "only-runtime");
+ if (testdir == NULL)
+ return;
+ ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &fn_repo_file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* write a flatpakref file */
+ fn_repourl = g_file_get_uri (fn_repo_file);
+ testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime");
+ if (testdir2 == NULL)
+ return;
+ testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2);
+ ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* convert it to a GsApp */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/*/org.test.Chiron/master"));
+ g_assert_true (gs_app_get_local_file (app) != NULL);
+
+ /* get runtime */
+ runtime = gs_app_get_runtime (app);
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* check the number of sources */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources = gs_plugin_loader_job_process (plugin_loader, plugin_job,
+ NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources != NULL);
+ g_assert_cmpint (gs_app_list_length (sources), ==, 0);
+
+ /* install, which will install the runtime from the new remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ NULL,
+ update_app_action_finish_sync,
+ loop);
+ g_main_loop_run (loop);
+ gs_test_flush_main_context ();
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+
+ /* check the number of sources */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources2 != NULL);
+ g_assert_cmpint (gs_app_list_length (sources2), ==, 1);
+
+ /* remove the app */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN);
+
+ /* remove the runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ app_source = gs_app_list_index (sources2, 0);
+ g_assert_true (app_source != NULL);
+ g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/test/*");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+}
+
+/* same as gs_plugins_flatpak_runtime_repo_func, but this time manually
+ * installing the flatpakrepo BEFORE the flatpakref is installed */
+static void
+gs_plugins_flatpak_runtime_repo_redundant_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app_source;
+ GsApp *runtime;
+ const gchar *fn_ref = "test.flatpakref";
+ const gchar *fn_repo = "test.flatpakrepo";
+ gboolean ret;
+ g_autofree gchar *fn_repourl = NULL;
+ g_autofree gchar *testdir2 = NULL;
+ g_autofree gchar *testdir2_repourl = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GFile) file_repo = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app_src = NULL;
+ g_autoptr(GsAppList) sources2 = NULL;
+ g_autoptr(GsAppList) sources = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* write a flatpakrepo file */
+ testdir = gs_test_get_filename (TESTDATADIR, "only-runtime");
+ if (testdir == NULL)
+ return;
+ ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &file_repo, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* convert it to a GsApp */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file_repo,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ NULL);
+ app_src = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app_src != NULL);
+ g_assert_cmpint (gs_app_get_kind (app_src), ==, AS_COMPONENT_KIND_REPOSITORY);
+ g_assert_cmpint (gs_app_get_state (app_src), ==, GS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app_src), ==, "test");
+ g_assert_cmpstr (gs_app_get_unique_id (app_src), ==, "*/*/*/test/master");
+ g_assert_true (gs_app_get_local_file (app_src) != NULL);
+
+ /* install the source manually */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_src, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);;
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_src), ==, GS_APP_STATE_INSTALLED);
+
+ /* write a flatpakref file */
+ fn_repourl = g_file_get_uri (file_repo);
+ testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime");
+ if (testdir2 == NULL)
+ return;
+ testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2);
+ ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* convert it to a GsApp */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/*/org.test.Chiron/master"));
+ g_assert_true (gs_app_get_local_file (app) != NULL);
+
+ /* get runtime */
+ runtime = gs_app_get_runtime (app);
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* check the number of sources */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources = gs_plugin_loader_job_process (plugin_loader, plugin_job,
+ NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources != NULL);
+ g_assert_cmpint (gs_app_list_length (sources), ==, 1); /* repo */
+
+ /* install, which will NOT install the runtime from the RuntimeRemote,
+ * but from the existing test repo */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+
+ /* check the number of sources */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources2 != NULL);
+
+ /* remove the app */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN);
+
+ /* remove the runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ app_source = gs_app_list_index (sources2, 0);
+ g_assert_true (app_source != NULL);
+ g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/test/*");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+}
+
+static void
+gs_plugins_flatpak_broken_remote_func (GsPluginLoader *plugin_loader)
+{
+ gboolean ret;
+ const gchar *fn = "test.flatpakref";
+ const gchar *fn_repo = "test.flatpakrepo";
+ g_autoptr(GFile) fn_repo_file = NULL;
+ g_autofree gchar *fn_repourl = NULL;
+ g_autofree gchar *testdir2 = NULL;
+ g_autofree gchar *testdir2_repourl = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsPlugin *plugin;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL);
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* add a remote with only the runtime in */
+ app_source = gs_flatpak_app_new ("test");
+ testdir = gs_test_get_filename (TESTDATADIR, "only-runtime");
+ if (testdir == NULL)
+ return;
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, "file:///wont/work");
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo=
+ * to avoid a warning) */
+ testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime");
+ if (testdir2 == NULL)
+ return;
+ ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* write a flatpakref file */
+ fn_repourl = g_file_get_uri (fn_repo_file);
+ testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2);
+ ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* convert it to a GsApp */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/test/org.test.Chiron/master"));
+ g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron");
+ g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis");
+ g_assert_cmpstr (gs_app_get_description (app), ==, "Long description.");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_true (gs_app_get_local_file (app) != NULL);
+
+ /* remove source */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+}
+
+static void
+flatpak_bundle_or_ref_helper (GsPluginLoader *plugin_loader,
+ gboolean is_bundle)
+{
+ GsApp *app_tmp;
+ GsApp *runtime;
+ gboolean ret;
+ GsPluginRefineFlags refine_flags;
+ g_autofree gchar *fn = NULL;
+ g_autofree gchar *testdir = NULL;
+ g_autofree gchar *testdir_repourl = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsApp) app2 = NULL;
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) search1 = NULL;
+ g_autoptr(GsAppList) search2 = NULL;
+ g_autoptr(GsAppList) sources = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsPlugin *plugin;
+ 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);
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* add a remote with only the runtime in */
+ app_source = gs_flatpak_app_new ("test");
+ testdir = gs_test_get_filename (TESTDATADIR, "only-runtime");
+ if (testdir == NULL)
+ return;
+ testdir_repourl = g_strdup_printf ("file://%s/repo", testdir);
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (0,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "runtime";
+ 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);
+ g_clear_object (&query);
+
+ list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (list != NULL);
+
+ /* make sure there is one entry, the flatpak runtime */
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ runtime = gs_app_list_index (list, 0);
+ g_assert_cmpstr (gs_app_get_id (runtime), ==, "org.test.Runtime");
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* install the runtime ahead of time */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", runtime,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+
+ if (is_bundle) {
+ /* find the flatpak bundle file */
+ fn = gs_test_get_filename (TESTDATADIR, "chiron.flatpak");
+ g_assert_true (fn != NULL);
+ file = g_file_new_for_path (fn);
+ refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE;
+ } else {
+ const gchar *fn_repo = "test.flatpakrepo";
+ g_autoptr(GFile) fn_repo_file = NULL;
+ g_autofree gchar *fn_repourl = NULL;
+ g_autofree gchar *testdir2 = NULL;
+ g_autofree gchar *testdir2_repourl = NULL;
+
+ /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo=
+ * to avoid a warning) */
+ testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime");
+ if (testdir2 == NULL)
+ return;
+ ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* write a flatpakref file */
+ fn_repourl = g_file_get_uri (fn_repo_file);
+ testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2);
+ fn = g_strdup ("test.flatpakref");
+ ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME;
+ }
+
+ /* Wait for the flatpak changes to be delivered through the file
+ monitor notifications, which will cleanup plugin cache. */
+ g_usleep (G_USEC_PER_SEC);
+
+ /* convert it to a GsApp */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", refine_flags,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (app != NULL);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron");
+ g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_true (gs_app_get_local_file (app) != NULL);
+ if (is_bundle) {
+ /* Note: The origin is set to "flatpak" here because an origin remote
+ * won't be created until the app is installed.
+ */
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/flatpak/org.test.Chiron/master"));
+ g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE_LOCAL);
+ } else {
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/test/org.test.Chiron/master"));
+ g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+ g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/");
+ g_assert_cmpstr (gs_app_get_description (app), ==, "Long description.");
+ }
+
+ /* get runtime */
+ runtime = gs_app_get_runtime (app);
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED);
+
+ /* install */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL);
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL);
+
+ /* search for the application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "chiron";
+ 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);
+ g_clear_object (&query);
+
+ search1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (search1 != NULL);
+ g_assert_cmpint (gs_app_list_length (search1), ==, 1);
+ app_tmp = gs_app_list_index (search1, 0);
+ g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "org.test.Chiron");
+
+ /* convert it to a GsApp again, and get the installed thing */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ NULL);
+ app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (app2 != NULL);
+ g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_INSTALLED);
+ if (is_bundle) {
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app2),
+ "user/flatpak/chiron-origin/org.test.Chiron/master"));
+ } else {
+ /* Note: the origin is now test-1 because that remote was created from the
+ * RuntimeRepo= setting
+ */
+ g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app2),
+ "user/flatpak/test-1/org.test.Chiron/master"));
+ }
+
+ /* remove app */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app2,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* remove runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* remove source */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ if (!is_bundle) {
+ /* remove remote added by RuntimeRepo= in flatpakref */
+ g_autoptr(GsApp) runtime_source = gs_flatpak_app_new ("test-1");
+ gs_app_set_kind (runtime_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (runtime_source, plugin);
+ gs_app_set_state (runtime_source, GS_APP_STATE_INSTALLED);
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (runtime_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ }
+
+ /* there should be no sources now */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL);
+ sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (sources != NULL);
+ g_assert_cmpint (gs_app_list_length (sources), ==, 0);
+
+ /* there should be no matches now */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "chiron";
+ 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);
+ g_clear_object (&query);
+
+ search2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (search2 != NULL);
+ g_assert_cmpint (gs_app_list_length (search2), ==, 0);
+}
+
+static void
+gs_plugins_flatpak_ref_func (GsPluginLoader *plugin_loader)
+{
+ flatpak_bundle_or_ref_helper (plugin_loader, FALSE);
+}
+
+static void
+gs_plugins_flatpak_bundle_func (GsPluginLoader *plugin_loader)
+{
+ flatpak_bundle_or_ref_helper (plugin_loader, TRUE);
+}
+
+static void
+gs_plugins_flatpak_count_signal_cb (GsPluginLoader *plugin_loader, guint *cnt)
+{
+ if (cnt != NULL)
+ (*cnt)++;
+}
+
+static void
+gs_plugins_flatpak_app_update_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app;
+ GsApp *app_tmp;
+ GsApp *runtime;
+ gboolean got_progress_installing = FALSE;
+ gboolean ret;
+ guint notify_progress_id;
+ guint notify_state_id;
+ guint pending_app_changed_cnt = 0;
+ guint pending_apps_changed_id;
+ guint progress_cnt = 0;
+ guint updates_changed_cnt = 0;
+ guint updates_changed_id;
+ g_autofree gchar *repodir1_fn = NULL;
+ g_autofree gchar *repodir2_fn = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsApp) old_runtime = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) list_updates = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
+ g_autofree gchar *repo_path = NULL;
+ g_autofree gchar *repo_url = NULL;
+ GsPlugin *plugin;
+ 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);
+
+ /* no flatpak, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak"))
+ return;
+
+ /* no files to use */
+ repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo");
+ if (repodir1_fn == NULL ||
+ !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+ repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-update/repo");
+ if (repodir2_fn == NULL ||
+ !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+
+ /* add indirection so we can switch this after install */
+ repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL);
+ unlink (repo_path);
+ g_assert_true (symlink (repodir1_fn, repo_path) == 0);
+
+ /* add a remote */
+ app_source = gs_flatpak_app_new ("test");
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ repo_url = g_strdup_printf ("file://%s", repo_path);
+ gs_flatpak_app_set_repo_url (app_source, repo_url);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ 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);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "Bingo";
+ query = gs_app_query_new ("keywords", keywords,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ "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);
+ g_clear_object (&query);
+
+ list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (list != NULL);
+
+ /* make sure there is one entry, the flatpak 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), ==, "org.test.Chiron");
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+
+ /* install, also installing runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL);
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL);
+
+ /* switch to the new repo */
+ g_assert_true (unlink (repo_path) == 0);
+ g_assert_true (symlink (repodir2_fn, repo_path) == 0);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (0, /* force now */
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* get the updates list */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS,
+ NULL);
+ list_updates = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_true (list_updates != NULL);
+
+ /* make sure there is one entry */
+ g_assert_cmpint (gs_app_list_length (list_updates), ==, 1);
+ for (guint i = 0; i < gs_app_list_length (list_updates); i++) {
+ app_tmp = gs_app_list_index (list_updates, i);
+ g_debug ("got update %s", gs_app_get_unique_id (app_tmp));
+ }
+
+ /* check that the runtime is not the update's one */
+ old_runtime = gs_app_get_runtime (app);
+ g_assert_true (old_runtime != NULL);
+ g_object_ref (old_runtime);
+ g_assert_cmpstr (gs_app_get_branch (old_runtime), !=, "new_master");
+
+ /* use the returned app, which can be a different object instance from previously */
+ app = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron/*");
+ g_assert_nonnull (app);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE);
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, "Version 1.2.4:\nThis is best.");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, "1.2.4");
+
+ /* care about signals */
+ pending_apps_changed_id =
+ g_signal_connect (plugin_loader, "pending-apps-changed",
+ G_CALLBACK (gs_plugins_flatpak_count_signal_cb),
+ &pending_app_changed_cnt);
+ updates_changed_id =
+ g_signal_connect (plugin_loader, "updates-changed",
+ G_CALLBACK (gs_plugins_flatpak_count_signal_cb),
+ &updates_changed_cnt);
+ notify_state_id =
+ g_signal_connect (app, "notify::state",
+ G_CALLBACK (update_app_state_notify_cb),
+ &got_progress_installing);
+ notify_progress_id =
+ g_signal_connect (app, "notify::progress",
+ G_CALLBACK (update_app_progress_notify_cb),
+ &progress_cnt);
+
+ /* use a mainloop so we get the events in the default context */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ NULL,
+ update_app_action_finish_sync,
+ loop);
+ g_main_loop_run (loop);
+ gs_test_flush_main_context ();
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.4");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL);
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL);
+ g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN ||
+ gs_app_get_progress (app) == 100);
+ g_assert_true (got_progress_installing);
+ //g_assert_cmpint (progress_cnt, >, 20); //FIXME: bug in OSTree
+ g_assert_cmpint (pending_app_changed_cnt, ==, 0);
+ g_assert_cmpint (updates_changed_cnt, ==, 1);
+
+ /* check that the app's runtime has changed */
+ runtime = gs_app_get_runtime (app);
+ g_assert_true (runtime != NULL);
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/new_master");
+ g_assert_true (old_runtime != runtime);
+ g_assert_cmpstr (gs_app_get_branch (runtime), ==, "new_master");
+ g_assert_true (gs_app_get_state (runtime) == GS_APP_STATE_INSTALLED);
+
+ /* no longer care */
+ g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id);
+ g_signal_handler_disconnect (plugin_loader, updates_changed_id);
+ g_signal_handler_disconnect (app, notify_state_id);
+ g_signal_handler_disconnect (app, notify_progress_id);
+
+ /* remove the app */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* remove the old_runtime */
+ g_assert_cmpstr (gs_app_get_unique_id (old_runtime), ==, "user/flatpak/test/org.test.Runtime/master");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", old_runtime,
+ NULL);
+ 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);
+
+ /* remove the runtime */
+ g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/new_master");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ 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);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+}
+
+static void
+gs_plugins_flatpak_runtime_extension_func (GsPluginLoader *plugin_loader)
+{
+ GsApp *app;
+ GsApp *runtime;
+ GsApp *app_tmp;
+ gboolean got_progress_installing = FALSE;
+ gboolean ret;
+ guint notify_progress_id;
+ guint notify_state_id;
+ guint pending_app_changed_cnt = 0;
+ guint pending_apps_changed_id;
+ guint progress_cnt = 0;
+ guint updates_changed_cnt = 0;
+ guint updates_changed_id;
+ g_autofree gchar *repodir1_fn = NULL;
+ g_autofree gchar *repodir2_fn = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app_source = NULL;
+ g_autoptr(GsApp) extension = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) list_updates = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
+ g_autofree gchar *repo_path = NULL;
+ g_autofree gchar *repo_url = NULL;
+ GsPlugin *plugin;
+ 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);
+
+ /* no flatpak, abort */
+ g_assert_true (gs_plugin_loader_get_enabled (plugin_loader, "flatpak"));
+
+ /* no files to use */
+ repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-extension/repo");
+ if (repodir1_fn == NULL ||
+ !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+ repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-extension-update/repo");
+ if (repodir2_fn == NULL ||
+ !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) {
+ g_test_skip ("no flatpak test repo");
+ return;
+ }
+
+ /* add indirection so we can switch this after install */
+ repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL);
+ g_assert_cmpint (symlink (repodir1_fn, repo_path), ==, 0);
+
+ /* add a remote */
+ app_source = gs_flatpak_app_new ("test");
+ gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY);
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak");
+ gs_app_set_management_plugin (app_source, plugin);
+ gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE);
+ repo_url = g_strdup_printf ("file://%s", repo_path);
+ gs_flatpak_app_set_repo_url (app_source, repo_url);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ 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);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "Bingo";
+ 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);
+ g_clear_object (&query);
+
+ 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 one entry, the flatpak 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), ==, "org.test.Chiron");
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+
+ /* install, also installing runtime and suggested extensions */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+
+ /* check if the extension was installed */
+ extension = gs_plugin_loader_app_create (plugin_loader,
+ "user/flatpak/*/org.test.Chiron.Extension/master",
+ NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_nonnull (extension);
+
+ g_assert_cmpint (gs_app_get_state (extension), ==, GS_APP_STATE_INSTALLED);
+
+ /* switch to the new repo (to get the update) */
+ g_assert_cmpint (unlink (repo_path), ==, 0);
+ g_assert_cmpint (symlink (repodir2_fn, repo_path), ==, 0);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_refresh_metadata_new (0, /* force now */
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* get the updates list */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS,
+ NULL);
+ list_updates = 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_updates);
+
+ g_assert_cmpint (gs_app_list_length (list_updates), ==, 1);
+ for (guint i = 0; i < gs_app_list_length (list_updates); i++) {
+ app_tmp = gs_app_list_index (list_updates, i);
+ g_debug ("got update %s", gs_app_get_unique_id (app_tmp));
+ }
+
+ /* check that the extension has no update */
+ app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron.Extension/*");
+ g_assert_null (app_tmp);
+
+ /* check that the app has an update (it's affected by the extension's update) */
+ app = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron/*");
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE);
+
+ /* care about signals */
+ pending_apps_changed_id =
+ g_signal_connect (plugin_loader, "pending-apps-changed",
+ G_CALLBACK (gs_plugins_flatpak_count_signal_cb),
+ &pending_app_changed_cnt);
+ updates_changed_id =
+ g_signal_connect (plugin_loader, "updates-changed",
+ G_CALLBACK (gs_plugins_flatpak_count_signal_cb),
+ &updates_changed_cnt);
+ notify_state_id =
+ g_signal_connect (app, "notify::state",
+ G_CALLBACK (update_app_state_notify_cb),
+ &got_progress_installing);
+ notify_progress_id =
+ g_signal_connect (app, "notify::progress",
+ G_CALLBACK (update_app_progress_notify_cb),
+ &progress_cnt);
+
+ /* use a mainloop so we get the events in the default context */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ NULL,
+ update_app_action_finish_sync,
+ loop);
+ g_main_loop_run (loop);
+ gs_test_flush_main_context ();
+
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_true (got_progress_installing);
+ g_assert_cmpint (pending_app_changed_cnt, ==, 0);
+
+ /* The install refreshes GsApp-s cache, thus re-get the extension */
+ g_clear_object (&extension);
+ extension = gs_plugin_loader_app_create (plugin_loader,
+ "user/flatpak/*/org.test.Chiron.Extension/master",
+ NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_nonnull (extension);
+
+ /* check the extension's state after the update */
+ g_assert_cmpint (gs_app_get_state (extension), ==, GS_APP_STATE_INSTALLED);
+
+ /* no longer care */
+ g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id);
+ g_signal_handler_disconnect (plugin_loader, updates_changed_id);
+ g_signal_handler_disconnect (app, notify_state_id);
+ g_signal_handler_disconnect (app, notify_progress_id);
+
+ g_clear_object (&list);
+ /* Reload the 'app', as it could change due to repo change */
+ g_object_unref (plugin_job);
+
+ keywords[0] = "Bingo";
+ query = gs_app_query_new ("keywords", keywords,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ "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);
+ g_clear_object (&query);
+
+ 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 one entry, the flatpak 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), ==, "org.test.Chiron");
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);
+
+ /* getting the runtime for later removal */
+ runtime = gs_app_get_runtime (app);
+ g_assert_nonnull (runtime);
+
+ /* remove the app */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* remove the runtime */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime,
+ NULL);
+ 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);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE);
+ 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);
+ g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE);
+
+ /* verify that the extension has been removed by the app's removal */
+ g_assert_false (gs_app_is_installed (extension));
+}
+
+int
+main (int argc, char **argv)
+{
+ g_autofree gchar *tmp_root = NULL;
+ gboolean ret;
+ int retval;
+ g_autofree gchar *xml = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsPluginLoader) plugin_loader = NULL;
+
+ /* 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);
+ g_setenv ("GS_XMLB_VERBOSE", "1", TRUE);
+ g_setenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD", "1", TRUE);
+
+ /* 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-flatpak-test-XXXXXX", NULL);
+ g_assert_true (tmp_root != NULL);
+ g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE);
+ g_setenv ("GS_SELF_TEST_FLATPAK_DATADIR", tmp_root, TRUE);
+
+ /* allow dist'ing with no gnome-software installed */
+ if (g_getenv ("GS_SELF_TEST_SKIP_ALL") != NULL)
+ return 0;
+
+ xml = g_strdup ("<?xml version=\"1.0\"?>\n"
+ "<components version=\"0.9\">\n"
+ " <component type=\"desktop\">\n"
+ " <id>zeus.desktop</id>\n"
+ " <name>Zeus</name>\n"
+ " <summary>A teaching application</summary>\n"
+ " </component>\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);
+ gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE);
+ 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/flatpak/app-with-runtime",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_app_with_runtime_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/app-missing-runtime",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_app_missing_runtime_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/ref",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_ref_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/bundle",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_bundle_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/broken-remote",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_broken_remote_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_runtime_repo_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo-redundant",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_runtime_repo_redundant_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/app-runtime-extension",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_runtime_extension_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/app-update-runtime",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_app_update_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/repo",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_repo_func);
+ g_test_add_data_func ("/gnome-software/plugins/flatpak/repo{non-ascii}",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_flatpak_repo_non_ascii_func);
+ retval = g_test_run ();
+
+ /* Clean up. */
+ gs_utils_rmtree (tmp_root, NULL);
+
+ return retval;
+}
diff --git a/plugins/flatpak/meson.build b/plugins/flatpak/meson.build
new file mode 100644
index 0000000..6a0baed
--- /dev/null
+++ b/plugins/flatpak/meson.build
@@ -0,0 +1,63 @@
+cargs = ['-DG_LOG_DOMAIN="GsPluginFlatpak"']
+deps = [
+ plugin_libs,
+ flatpak,
+ libxmlb,
+ ostree,
+]
+
+if get_option('mogwai')
+ deps += mogwai_schedule_client
+endif
+
+shared_module(
+ 'gs_plugin_flatpak',
+ sources : [
+ 'gs-flatpak-app.c',
+ 'gs-flatpak.c',
+ 'gs-flatpak-transaction.c',
+ 'gs-flatpak-utils.c',
+ 'gs-plugin-flatpak.c'
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : deps,
+)
+metainfo = 'org.gnome.Software.Plugin.Flatpak.metainfo.xml'
+
+i18n.merge_file(
+ input: metainfo + '.in',
+ output: metainfo,
+ type: 'xml',
+ po_dir: join_paths(meson.project_source_root(), 'po'),
+ install: true,
+ install_dir: join_paths(get_option('datadir'), 'metainfo')
+)
+
+if get_option('tests')
+ subdir('tests')
+
+ cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
+ cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"']
+ cargs += ['-DTESTDATADIR="' + join_paths(meson.current_build_dir(), 'tests') + '"']
+ e = executable(
+ 'gs-self-test-flatpak',
+ compiled_schemas,
+ sources : [
+ 'gs-flatpak-app.c',
+ 'gs-self-test.c'
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ dependencies : deps,
+ c_args : cargs,
+ )
+ test('gs-self-test-flatpak', e, suite: ['plugins', 'flatpak'], env: test_env, timeout : 120)
+endif
diff --git a/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in
new file mode 100644
index 0000000..44d6d03
--- /dev/null
+++ b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> -->
+<component type="addon">
+ <id>org.gnome.Software.Plugin.Flatpak</id>
+ <extends>org.gnome.Software.desktop</extends>
+ <name>Flatpak Support</name>
+ <summary>Flatpak is a framework for desktop applications on Linux</summary>
+ <url type="homepage">http://flatpak.org/</url>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <update_contact>richard_at_hughsie.com</update_contact>
+</component>
diff --git a/plugins/flatpak/tests/app-extension-update/.gitignore b/plugins/flatpak/tests/app-extension-update/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore
new file mode 100644
index 0000000..db00ec8
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore
@@ -0,0 +1 @@
+files/share/app-info
diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty
diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README
new file mode 100644
index 0000000..a0b9703
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README
@@ -0,0 +1 @@
+UPDATED! \ No newline at end of file
diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml
new file mode 100644
index 0000000..d884539
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2017 Endless Mobile, Inc.
+ Author: Joaquim Rocha <jrocha@endlessm.com>
+-->
+<component type="runtime">
+ <id>org.test.Chiron.Extension</id>
+ <metadata_license>CC0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Chiron App Extension</name>
+ <summary>Test extension for flatpak self tests</summary>
+</component>
+
diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata
new file mode 100644
index 0000000..d81f8f9
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata
@@ -0,0 +1,6 @@
+[Runtime]
+name=org.test.Chiron.Extension
+sdk=org.test.Runtime/x86_64/master
+
+[ExtensionOf]
+ref=app/org.test.Chiron/x86_64/master
diff --git a/plugins/flatpak/tests/app-extension/.gitignore b/plugins/flatpak/tests/app-extension/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore
new file mode 100644
index 0000000..db00ec8
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore
@@ -0,0 +1 @@
+files/share/app-info
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml
new file mode 100644
index 0000000..d884539
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2017 Endless Mobile, Inc.
+ Author: Joaquim Rocha <jrocha@endlessm.com>
+-->
+<component type="runtime">
+ <id>org.test.Chiron.Extension</id>
+ <metadata_license>CC0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Chiron App Extension</name>
+ <summary>Test extension for flatpak self tests</summary>
+</component>
+
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata
new file mode 100644
index 0000000..d81f8f9
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata
@@ -0,0 +1,6 @@
+[Runtime]
+name=org.test.Chiron.Extension
+sdk=org.test.Runtime/x86_64/master
+
+[ExtensionOf]
+ref=app/org.test.Chiron/x86_64/master
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore
new file mode 100644
index 0000000..fea15c0
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore
@@ -0,0 +1,2 @@
+export
+files/share/app-info
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh
new file mode 100755
index 0000000..e61d501
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Hello world"
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
new file mode 100644
index 0000000..0d912a8
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
+<component type="desktop">
+ <id>org.test.Chiron.desktop</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Chiron</name>
+ <summary>Single line synopsis</summary>
+ <description><p>Long description.</p></description>
+ <url type="homepage">http://127.0.0.1/</url>
+ <releases>
+ <release date="2014-12-15" version="1.2.3">
+ <description><p>This is better.</p></description>
+ </release>
+ </releases>
+</component>
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
new file mode 100644
index 0000000..2fbdf95
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Type=Application
+Name=Chiron
+Exec=chiron.sh
+Icon=org.test.Chiron
+Keywords=Bingo;
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
new file mode 100644
index 0000000..0c38f2f
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
Binary files differ
diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata
new file mode 100644
index 0000000..45b76d6
--- /dev/null
+++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata
@@ -0,0 +1,10 @@
+[Application]
+name=org.test.Chiron
+runtime=org.test.Runtime/x86_64/master
+command=chiron.sh
+
+[Extension org.test.Chiron.Extension]
+directory=share/extension
+subdirectories=true
+version=master
+autodelete=true
diff --git a/plugins/flatpak/tests/app-missing-runtime/.gitignore b/plugins/flatpak/tests/app-missing-runtime/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/app-missing-runtime/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron
new file mode 120000
index 0000000..d9384e4
--- /dev/null
+++ b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron
@@ -0,0 +1 @@
+../app-with-runtime/org.test.Chiron/ \ No newline at end of file
diff --git a/plugins/flatpak/tests/app-update/.gitignore b/plugins/flatpak/tests/app-update/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore
new file mode 100644
index 0000000..fea15c0
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore
@@ -0,0 +1,2 @@
+export
+files/share/app-info
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh
new file mode 100644
index 0000000..dfed21c
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Hello world, with upgrades"
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
new file mode 100644
index 0000000..74eb9db
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
+<component type="desktop">
+ <id>org.test.Chiron</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Chiron</name>
+ <summary>Single line synopsis</summary>
+ <description><p>Long description.</p></description>
+ <url type="homepage">http://127.0.0.1/</url>
+ <releases>
+ <release date="2015-02-13" version="1.2.4">
+ <description><p>This is best.</p></description>
+ </release>
+ <release date="2014-12-15" version="1.2.3">
+ <description><p>This is better.</p></description>
+ </release>
+ </releases>
+</component>
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
new file mode 120000
index 0000000..2b06818
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
@@ -0,0 +1 @@
+../../../../../app-missing-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop \ No newline at end of file
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
new file mode 120000
index 0000000..9c37986
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
@@ -0,0 +1 @@
+../../../../../../../../app-missing-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png \ No newline at end of file
diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/metadata b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata
new file mode 100644
index 0000000..1de0ab8
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata
@@ -0,0 +1,4 @@
+[Application]
+name=org.test.Chiron
+runtime=org.test.Runtime/x86_64/new_master
+command=chiron.sh
diff --git a/plugins/flatpak/tests/app-with-runtime/.gitignore b/plugins/flatpak/tests/app-with-runtime/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore
new file mode 100644
index 0000000..fea15c0
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore
@@ -0,0 +1,2 @@
+export
+files/share/app-info
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh
new file mode 100755
index 0000000..e61d501
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Hello world"
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
new file mode 100644
index 0000000..58af082
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
+<component type="desktop">
+ <id>org.test.Chiron</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Chiron</name>
+ <summary>Single line synopsis</summary>
+ <description><p>Long description.</p></description>
+ <url type="homepage">http://127.0.0.1/</url>
+ <releases>
+ <release date="2014-12-15" version="1.2.3">
+ <description><p>This is better.</p></description>
+ </release>
+ </releases>
+</component>
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
new file mode 100644
index 0000000..b744766
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=Chiron
+Exec=chiron.sh
+Icon=org.test.Chiron
+Keywords=Bingo;
+X-Flatpak=org.test.Chiron
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
new file mode 100644
index 0000000..0c38f2f
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
Binary files differ
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata
new file mode 100644
index 0000000..ce57357
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata
@@ -0,0 +1,4 @@
+[Application]
+name=org.test.Chiron
+runtime=org.test.Runtime/x86_64/master
+command=chiron.sh
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata
new file mode 100644
index 0000000..16f0fa1
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata
@@ -0,0 +1,3 @@
+[Runtime]
+name=org.test.Runtime
+sdk=org.test.Runtime/x86_64/master
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore
new file mode 100644
index 0000000..3600b9c
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore
@@ -0,0 +1 @@
+app-info
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README
diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml
new file mode 100644
index 0000000..5d68c60
--- /dev/null
+++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2017 Richard Hughes <richard@hughsie.com> -->
+<component type="runtime">
+ <id>org.test.Runtime</id>
+ <metadata_license>CC0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Test runtime</name>
+ <summary>Test runtime for flatpak self tests</summary>
+</component>
+
diff --git a/plugins/flatpak/tests/build.py b/plugins/flatpak/tests/build.py
new file mode 100755
index 0000000..6c6a8dd
--- /dev/null
+++ b/plugins/flatpak/tests/build.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python3
+
+import subprocess
+import os
+import shutil
+import configparser
+
+def build_flatpak(appid, srcdir, repodir, branch='master', cleanrepodir=True):
+ print('Building %s from %s into %s' % (appid, srcdir, repodir))
+
+ # delete repodir
+ if cleanrepodir and os.path.exists(repodir):
+ print("Deleting %s" % repodir)
+ shutil.rmtree(repodir)
+
+ # delete exportdir
+ exportdir = os.path.join(srcdir, appid, 'export')
+ if os.path.exists(exportdir):
+ print("Deleting %s" % exportdir)
+ shutil.rmtree(exportdir)
+
+ metadata_path = os.path.join(srcdir, appid, 'metadata')
+ metadata = configparser.ConfigParser()
+ metadata.read(metadata_path)
+ is_runtime = True if 'Runtime' in metadata.sections() else False
+ is_extension = True if 'ExtensionOf' in metadata.sections() else False
+
+ # runtimes have different defaults
+ if is_runtime and not is_extension:
+ prefix = 'usr'
+ else:
+ prefix = 'files'
+
+ # finish the build
+ argv = ['flatpak', 'build-finish']
+ argv.append(os.path.join(srcdir, appid))
+ subprocess.call(argv)
+
+ # compose AppStream data
+ argv = ['appstream-compose']
+ argv.append('--origin=flatpak')
+ argv.append('--basename=%s' % appid)
+ argv.append('--prefix=%s' % os.path.join(srcdir, appid, prefix))
+ argv.append('--output-dir=%s' % os.path.join(srcdir, appid, prefix, 'share/app-info/xmls'))
+ argv.append(appid)
+ subprocess.call(argv)
+
+ # export into repo
+ argv = ['flatpak', 'build-export']
+ argv.append(repodir)
+ argv.append(os.path.join(srcdir, appid))
+ argv.append(branch)
+ argv.append('--update-appstream')
+ argv.append('--timestamp=2016-09-15T01:02:03')
+ if is_runtime:
+ argv.append('--runtime')
+ subprocess.call(argv)
+
+def build_flatpak_bundle(appid, srcdir, repodir, filename, branch='master'):
+ argv = ['flatpak', 'build-bundle']
+ argv.append(repodir)
+ argv.append(filename)
+ argv.append(appid)
+ argv.append(branch)
+ subprocess.call(argv)
+
+def copy_repo(srcdir, destdir):
+ srcdir_repo = os.path.join(srcdir, 'repo')
+ destdir_repo = os.path.join(destdir, 'repo')
+ print("Copying %s to %s" % (srcdir_repo, destdir_repo))
+ if os.path.exists(destdir_repo):
+ shutil.rmtree(destdir_repo)
+ shutil.copytree(srcdir_repo, destdir_repo)
+
+# normal app with runtime in same remote
+build_flatpak('org.test.Chiron',
+ 'app-with-runtime',
+ 'app-with-runtime/repo')
+build_flatpak('org.test.Runtime',
+ 'app-with-runtime',
+ 'app-with-runtime/repo',
+ cleanrepodir=False)
+
+# build a flatpak bundle for the app
+build_flatpak_bundle('org.test.Chiron',
+ 'app-with-runtime',
+ 'app-with-runtime/repo',
+ 'chiron.flatpak')
+
+# app referencing runtime that cannot be found
+build_flatpak('org.test.Chiron',
+ 'app-with-runtime',
+ 'app-missing-runtime/repo')
+
+# app with an update
+build_flatpak('org.test.Runtime',
+ 'app-with-runtime',
+ 'app-update/repo',
+ branch='new_master',
+ cleanrepodir=True)
+build_flatpak('org.test.Chiron',
+ 'app-update',
+ 'app-update/repo',
+ cleanrepodir=False)
+
+# just a runtime present
+build_flatpak('org.test.Runtime',
+ 'only-runtime',
+ 'only-runtime/repo')
+
+# app with an extension
+copy_repo('only-runtime', 'app-extension')
+build_flatpak('org.test.Chiron',
+ 'app-extension',
+ 'app-extension/repo',
+ cleanrepodir=False)
+build_flatpak('org.test.Chiron.Extension',
+ 'app-extension',
+ 'app-extension/repo',
+ cleanrepodir=False)
+copy_repo('app-extension', 'app-extension-update')
+build_flatpak('org.test.Chiron.Extension',
+ 'app-extension-update',
+ 'app-extension-update/repo',
+ cleanrepodir=False)
diff --git a/plugins/flatpak/tests/chiron.flatpak b/plugins/flatpak/tests/chiron.flatpak
new file mode 100644
index 0000000..ce038e9
--- /dev/null
+++ b/plugins/flatpak/tests/chiron.flatpak
Binary files differ
diff --git a/plugins/flatpak/tests/flatpakrepos.tar.gz b/plugins/flatpak/tests/flatpakrepos.tar.gz
new file mode 100644
index 0000000..f8bcfde
--- /dev/null
+++ b/plugins/flatpak/tests/flatpakrepos.tar.gz
Binary files differ
diff --git a/plugins/flatpak/tests/meson.build b/plugins/flatpak/tests/meson.build
new file mode 100644
index 0000000..9e48b00
--- /dev/null
+++ b/plugins/flatpak/tests/meson.build
@@ -0,0 +1,34 @@
+tar = find_program('tar')
+custom_target(
+ 'flatpak-self-test-data',
+ input : 'flatpakrepos.tar.gz',
+ output : 'done',
+ command : [
+ tar,
+ '--no-same-owner',
+ '--directory=' + meson.current_build_dir(),
+ '-xf', '@INPUT@',
+ ],
+ build_by_default : true,
+)
+
+custom_target(
+ 'flatpak-self-test-bundle',
+ output : 'flatpakrepos.tar.gz',
+ command : [
+ tar,
+ '-czf', '@OUTPUT@',
+ 'app-missing-runtime/repo/',
+ 'app-update/repo/',
+ 'app-with-runtime/repo/',
+ 'only-runtime/repo/',
+ 'app-extension/repo',
+ 'app-extension-update/repo',
+ ],
+)
+
+configure_file(
+ input : 'chiron.flatpak',
+ output : 'chiron.flatpak',
+ copy : true,
+)
diff --git a/plugins/flatpak/tests/only-runtime/.gitignore b/plugins/flatpak/tests/only-runtime/.gitignore
new file mode 100644
index 0000000..f606d5e
--- /dev/null
+++ b/plugins/flatpak/tests/only-runtime/.gitignore
@@ -0,0 +1 @@
+repo
diff --git a/plugins/flatpak/tests/only-runtime/org.test.Runtime b/plugins/flatpak/tests/only-runtime/org.test.Runtime
new file mode 120000
index 0000000..eb7054c
--- /dev/null
+++ b/plugins/flatpak/tests/only-runtime/org.test.Runtime
@@ -0,0 +1 @@
+../app-with-runtime/org.test.Runtime/ \ No newline at end of file