summaryrefslogtreecommitdiffstats
path: root/plugins/flatpak
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/flatpak')
l---------plugins/flatpak/gs-appstream.c1
l---------plugins/flatpak/gs-appstream.h1
-rw-r--r--plugins/flatpak/gs-flatpak-app.c178
-rw-r--r--plugins/flatpak/gs-flatpak-app.h62
-rw-r--r--plugins/flatpak/gs-flatpak-transaction.c804
-rw-r--r--plugins/flatpak/gs-flatpak-transaction.h35
-rw-r--r--plugins/flatpak/gs-flatpak-utils.c212
-rw-r--r--plugins/flatpak/gs-flatpak-utils.h21
-rw-r--r--plugins/flatpak/gs-flatpak.c3258
-rw-r--r--plugins/flatpak/gs-flatpak.h128
-rw-r--r--plugins/flatpak/gs-plugin-flatpak.c1320
-rw-r--r--plugins/flatpak/gs-self-test.c1936
-rw-r--r--plugins/flatpak/meson.build70
-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
l---------plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore1
-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
59 files changed, 8353 insertions, 0 deletions
diff --git a/plugins/flatpak/gs-appstream.c b/plugins/flatpak/gs-appstream.c
new file mode 120000
index 0000000..96326ab
--- /dev/null
+++ b/plugins/flatpak/gs-appstream.c
@@ -0,0 +1 @@
+../core/gs-appstream.c \ No newline at end of file
diff --git a/plugins/flatpak/gs-appstream.h b/plugins/flatpak/gs-appstream.h
new file mode 120000
index 0000000..4eabcb3
--- /dev/null
+++ b/plugins/flatpak/gs-appstream.h
@@ -0,0 +1 @@
+../core/gs-appstream.h \ No newline at end of file
diff --git a/plugins/flatpak/gs-flatpak-app.c b/plugins/flatpak/gs-flatpak-app.c
new file mode 100644
index 0000000..cf98248
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-app.c
@@ -0,0 +1,178 @@
+/* -*- 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");
+}
diff --git a/plugins/flatpak/gs-flatpak-app.h b/plugins/flatpak/gs-flatpak-app.h
new file mode 100644
index 0000000..ab6c10a
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-app.h
@@ -0,0 +1,62 @@
+/* -*- 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);
+
+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..ffff22e
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-transaction.c
@@ -0,0 +1,804 @@
+/* -*- 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;
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+ gboolean no_deploy;
+#endif
+};
+
+
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+typedef enum {
+ PROP_NO_DEPLOY = 1,
+} GsFlatpakTransactionProperty;
+#endif
+
+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);
+}
+
+
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+void
+gs_flatpak_transaction_set_no_deploy (FlatpakTransaction *transaction, gboolean no_deploy)
+{
+ GsFlatpakTransaction *self;
+
+ g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction));
+
+ self = GS_FLATPAK_TRANSACTION (transaction);
+ if (self->no_deploy == no_deploy)
+ return;
+ self->no_deploy = no_deploy;
+ flatpak_transaction_set_no_deploy (transaction, no_deploy);
+
+ g_object_notify (G_OBJECT (self), "no-deploy");
+}
+#endif
+
+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);
+ 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)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ }
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+ /* 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);
+ }
+#endif /* flatpak ≥ 1.7.3 */
+ }
+ 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)
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+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) != AS_APP_STATE_INSTALLING &&
+ gs_app_get_state (root_app) != AS_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));
+ }
+}
+#endif /* flatpak 1.7.3 */
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+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);
+ }
+}
+#endif /* flatpak 1.7.3 */
+
+static void
+_transaction_progress_changed_cb (FlatpakTransactionProgress *progress,
+ gpointer user_data)
+{
+ ProgressData *data = user_data;
+ GsApp *app = data->app;
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+ GsFlatpakTransaction *self = data->transaction;
+ g_autolist(FlatpakTransactionOperation) ops = NULL;
+#else
+ guint percent;
+#endif
+
+ 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;
+ }
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+ /* 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);
+#else /* if !flatpak 1.7.3 */
+ percent = flatpak_transaction_progress_get_progress (progress);
+
+ if (gs_app_get_progress (app) != 100 &&
+ gs_app_get_progress (app) != GS_APP_PROGRESS_UNKNOWN &&
+ gs_app_get_progress (app) > percent) {
+ g_warning ("ignoring percentage %u%% -> %u%% as going down...",
+ gs_app_get_progress (app), percent);
+ return;
+ }
+
+ gs_app_set_progress (app, percent);
+#endif /* !flatpak 1.7.3 */
+}
+
+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) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL);
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UPDATE:
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ break;
+ default:
+ break;
+ }
+}
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+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, AS_APP_STATE_INSTALLED);
+ }
+ }
+}
+#endif /* flatpak 1.7.3 */
+
+static void
+_transaction_operation_done (FlatpakTransaction *transaction,
+ FlatpakTransactionOperation *operation,
+ const gchar *commit,
+ FlatpakTransactionResult details)
+{
+#if !FLATPAK_CHECK_VERSION(1,5,1) || FLATPAK_CHECK_VERSION(1,7,3)
+ GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
+#endif
+ /* 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, AS_APP_STATE_INSTALLED);
+
+#if FLATPAK_CHECK_VERSION(1,7,3)
+ set_skipped_related_apps_to_installed (self, transaction, operation);
+#endif
+ break;
+ case FLATPAK_TRANSACTION_OPERATION_UPDATE:
+ gs_app_set_version (app, gs_app_get_update_version (app));
+ gs_app_set_update_details (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_CHECK_VERSION(1,5,1)
+ if (self->no_deploy)
+#else
+ if (flatpak_transaction_get_no_deploy (transaction))
+#endif
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+ else
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+#if FLATPAK_CHECK_VERSION(1,7,3)
+ set_skipped_related_apps_to_installed (self, transaction, operation);
+#endif
+ 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, AS_APP_STATE_UNKNOWN);
+ break;
+ default:
+ gs_app_set_state (app, AS_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_printerr ("%s is end-of-life, in preference of %s\n", ref, rebase);
+ } else if (reason) {
+ g_printerr ("%s is end-of-life, with reason: %s\n", ref, reason);
+ }
+ //FIXME: show something in the UI
+}
+
+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;
+}
+
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+static void
+gs_flatpak_transaction_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ FlatpakTransaction *transaction = FLATPAK_TRANSACTION (object);
+
+ switch ((GsFlatpakTransactionProperty) prop_id) {
+ case PROP_NO_DEPLOY:
+ gs_flatpak_transaction_set_no_deploy (transaction, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+#endif
+
+static void
+gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass)
+{
+
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+ GParamSpec *pspec;
+#endif
+ 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;
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+ object_class->set_property = gs_flatpak_transaction_set_property;
+
+ pspec = g_param_spec_boolean ("no-deploy", NULL,
+ "Whether the current transaction will deploy the downloaded objects",
+ FALSE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_NO_DEPLOY, pspec);
+#endif
+
+ 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..97a4e10
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-transaction.h
@@ -0,0 +1,35 @@
+/* -*- 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);
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+void gs_flatpak_transaction_set_no_deploy (FlatpakTransaction *transaction,
+ gboolean no_deploy);
+#endif
+
+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..4dee104
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-utils.c
@@ -0,0 +1,212 @@
+/* -*- 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 "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:
+ case FLATPAK_ERROR_REMOTE_NOT_FOUND:
+ case FLATPAK_ERROR_RUNTIME_NOT_FOUND:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ 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 (FlatpakRemote *xremote)
+{
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *url = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ app = gs_flatpak_app_new (flatpak_remote_get_name (xremote));
+ gs_app_set_kind (app, AS_APP_KIND_SOURCE);
+ gs_app_set_state (app, flatpak_remote_get_disabled (xremote) ?
+ AS_APP_STATE_AVAILABLE : AS_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_APP_SIZE_UNKNOWABLE);
+
+ /* title */
+ title = flatpak_remote_get_title (xremote);
+ if (title != NULL)
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, title);
+
+ /* url */
+ url = flatpak_remote_get_url (xremote);
+ if (url != NULL)
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
+
+ /* 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_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);
+
+ /* ensure this is valid for flatpak */
+ repo_id = g_str_to_ascii (basename, NULL);
+ tmp = g_strrstr (repo_id, ".");
+ if (tmp != NULL)
+ *tmp = '\0';
+ 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_APP_KIND_SOURCE);
+ gs_app_set_state (app, AS_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_APP_SIZE_UNKNOWABLE);
+ gs_flatpak_app_set_repo_url (app, repo_url);
+ 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_autoptr(AsIcon) ic = as_icon_new ();
+ as_icon_set_kind (ic, AS_ICON_KIND_REMOTE);
+ as_icon_set_url (ic, repo_icon);
+ gs_app_add_icon (app, ic);
+ }
+
+ /* success */
+ return g_steal_pointer (&app);
+}
diff --git a/plugins/flatpak/gs-flatpak-utils.h b/plugins/flatpak/gs-flatpak-utils.h
new file mode 100644
index 0000000..61cd62d
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak-utils.h
@@ -0,0 +1,21 @@
+/* -*- 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 (FlatpakRemote *xremote);
+GsApp *gs_flatpak_app_new_from_repo_file (GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c
new file mode 100644
index 0000000..cf86a7f
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak.c
@@ -0,0 +1,3258 @@
+/* -*- 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
+ */
+
+#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;
+ GPtrArray *installed_refs; /* must be entirely replaced rather than updated internally */
+ GMutex installed_refs_mutex;
+ GHashTable *broken_remotes;
+ GMutex broken_remotes_mutex;
+ GFileMonitor *monitor;
+ AsAppScope scope;
+ GsPlugin *plugin;
+ XbSilo *silo;
+ GRWLock silo_lock;
+ gchar *id;
+ guint changed_id;
+ GHashTable *app_silos;
+ GMutex app_silos_mutex;
+};
+
+G_DEFINE_TYPE (GsFlatpak, gs_flatpak, G_TYPE_OBJECT)
+
+static gboolean
+gs_flatpak_refresh_appstream (GsFlatpak *self, guint cache_age,
+ GCancellable *cancellable, GError **error);
+
+static void
+gs_plugin_refine_item_scope (GsFlatpak *self, GsApp *app)
+{
+ if (gs_app_get_scope (app) == AS_APP_SCOPE_UNKNOWN) {
+ gboolean is_user = flatpak_installation_get_is_user (self->installation);
+ gs_app_set_scope (app, is_user ? AS_APP_SCOPE_USER : AS_APP_SCOPE_SYSTEM);
+ }
+}
+
+static void
+gs_flatpak_claim_app (GsFlatpak *self, GsApp *app)
+{
+ if (gs_app_get_management_plugin (app) != NULL)
+ return;
+ gs_app_set_management_plugin (app, gs_plugin_get_name (self->plugin));
+ gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_FLATPAK);
+
+ /* 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_claim_app_list (GsFlatpak *self, GsAppList *list)
+{
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ 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_APP_KIND_DESKTOP);
+ } 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_APP_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_APP_KIND_GENERIC);
+ } else {
+ gs_app_set_kind (app, AS_APP_KIND_RUNTIME);
+ }
+ }
+}
+
+static GsAppPermissions
+perms_from_metadata (GKeyFile *keyfile)
+{
+ char **strv;
+ char *str;
+ GsAppPermissions permissions = GS_APP_PERMISSIONS_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"))
+ permissions |= GS_APP_PERMISSIONS_SYSTEM_BUS;
+ if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "session-bus"))
+ permissions |= GS_APP_PERMISSIONS_SESSION_BUS;
+ if (strv != NULL &&
+ !g_strv_contains ((const gchar * const*)strv, "fallback-x11") &&
+ g_strv_contains ((const gchar * const*)strv, "x11"))
+ permissions |= GS_APP_PERMISSIONS_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"))
+ permissions |= GS_APP_PERMISSIONS_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"))
+ permissions |= GS_APP_PERMISSIONS_NETWORK;
+ g_strfreev (strv);
+
+ strv = g_key_file_get_string_list (keyfile, "Context", "filesystems", NULL, NULL);
+ if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "home") ||
+ g_strv_contains ((const gchar * const *)strv, "home:rw")))
+ permissions |= GS_APP_PERMISSIONS_HOME_FULL;
+ else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "home:ro"))
+ permissions |= GS_APP_PERMISSIONS_HOME_READ;
+ if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "host") ||
+ g_strv_contains ((const gchar * const *)strv, "host:rw")))
+ permissions |= GS_APP_PERMISSIONS_FILESYSTEM_FULL;
+ else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "host:ro"))
+ permissions |= GS_APP_PERMISSIONS_FILESYSTEM_READ;
+ if (strv != NULL && (g_strv_contains ((const gchar * const *)strv, "xdg-download") ||
+ g_strv_contains ((const gchar * const *)strv, "xdg-download:rw")))
+ permissions |= GS_APP_PERMISSIONS_DOWNLOADS_FULL;
+ else if (strv != NULL && g_strv_contains ((const gchar * const *)strv, "xdg-download:ro"))
+ permissions |= GS_APP_PERMISSIONS_DOWNLOADS_READ;
+ 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"))
+ permissions |= GS_APP_PERMISSIONS_SETTINGS;
+ g_free (str);
+
+ str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.Flatpak", NULL);
+ if (str != NULL && g_str_equal (str, "talk"))
+ permissions |= GS_APP_PERMISSIONS_ESCAPE_SANDBOX;
+ g_free (str);
+
+ /* no permissions set */
+ if (permissions == GS_APP_PERMISSIONS_UNKNOWN)
+ return GS_APP_PERMISSIONS_NONE;
+
+ return permissions;
+}
+
+static void
+gs_flatpak_set_update_permissions (GsFlatpak *self, GsApp *app, FlatpakInstalledRef *xref)
+{
+ g_autoptr(GBytes) old_bytes = NULL;
+ g_autoptr(GKeyFile) old_keyfile = NULL;
+ g_autoptr(GBytes) bytes = NULL;
+ g_autoptr(GKeyFile) keyfile = NULL;
+ GsAppPermissions permissions;
+ 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 (self->installation,
+ gs_app_get_origin (app),
+ FLATPAK_REF (xref),
+ NULL,
+ &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);
+ permissions = GS_APP_PERMISSIONS_UNKNOWN;
+ } else {
+ 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);
+ permissions = perms_from_metadata (keyfile) & ~perms_from_metadata (old_keyfile);
+ }
+
+ /* no new permissions set */
+ if (permissions == GS_APP_PERMISSIONS_UNKNOWN)
+ permissions = GS_APP_PERMISSIONS_NONE;
+
+ gs_app_set_update_permissions (app, permissions);
+
+ if (permissions != GS_APP_PERMISSIONS_NONE)
+ gs_app_add_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));
+
+ /* 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_APP_KIND_UNKNOWN ||
+ gs_app_get_kind (app) == AS_APP_KIND_GENERIC) {
+ gs_flatpak_set_kind_from_flatpak (app, xref);
+ }
+}
+
+static GsApp *
+gs_flatpak_create_app (GsFlatpak *self, const gchar *origin, FlatpakRef *xref)
+{
+ 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_app_set_origin (app, origin);
+
+ /* return 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;
+
+ /* fallback values */
+ if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) {
+ g_autoptr(AsIcon) 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 = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
+ as_icon_set_name (icon, "system-run-symbolic");
+ 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_unique_id_equal() as the equal func and a NULL
+ * origin becomes a "*" in as_utils_unique_id_build().
+ */
+ if (origin != NULL)
+ 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 (xremote);
+ 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_plugin_flatpak_changed_cb (GFileMonitor *monitor,
+ GFile *child,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ GsFlatpak *self)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ /* manually drop the cache */
+ if (!flatpak_installation_drop_caches (self->installation,
+ NULL, &error)) {
+ g_warning ("failed to drop cache: %s", error->message);
+ return;
+ }
+
+ /* drop the installed refs cache */
+ locker = g_mutex_locker_new (&self->installed_refs_mutex);
+ g_clear_pointer (&self->installed_refs, g_ptr_array_unref);
+}
+
+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 !FLATPAK_CHECK_VERSION(1,1,1)
+static gchar *
+gs_flatpak_get_xremote_main_ref (GsFlatpak *self, FlatpakRemote *xremote, GError **error)
+{
+ g_autoptr(GFile) dir = NULL;
+ g_autofree gchar *dir_path = NULL;
+ g_autofree gchar *config_fn = NULL;
+ g_autofree gchar *group = NULL;
+ g_autofree gchar *main_ref = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+
+ /* figure out the path to the config keyfile */
+ dir = flatpak_installation_get_path (self->installation);
+ if (dir == NULL)
+ return NULL;
+ dir_path = g_file_get_path (dir);
+ if (dir_path == NULL)
+ return NULL;
+ config_fn = g_build_filename (dir_path, "repo", "config", NULL);
+
+ kf = g_key_file_new ();
+ if (!g_key_file_load_from_file (kf, config_fn, G_KEY_FILE_NONE, error))
+ return NULL;
+
+ group = g_strdup_printf ("remote \"%s\"", flatpak_remote_get_name (xremote));
+ main_ref = g_key_file_get_string (kf, group, "xa.main-ref", error);
+ return g_steal_pointer (&main_ref);
+}
+#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;
+
+ /* 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 (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_add_apps_from_xremote (GsFlatpak *self,
+ XbBuilder *builder,
+ FlatpakRemote *xremote,
+ 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 ();
+
+ /* get the AppStream data location */
+ appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL);
+ if (appstream_dir == NULL) {
+ g_debug ("no appstream dir for %s, skipping",
+ flatpak_remote_get_name (xremote));
+ return 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_debug ("no %s appstream metadata found: %s",
+ flatpak_remote_get_name (xremote),
+ 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, flatpak_remote_get_name (xremote));
+
+ /* 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_app_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;
+#if FLATPAK_CHECK_VERSION(1,1,1)
+ main_ref = flatpak_remote_get_main_ref (xremote);
+#else
+ g_autoptr(GError) error_local = NULL;
+ main_ref = gs_flatpak_get_xremote_main_ref (self, xremote, &error_local);
+ if (main_ref == NULL) {
+ g_warning ("failed to get main ref: %s", error_local->message);
+ g_clear_error (&error_local);
+ }
+#endif
+ 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)
+{
+ GString *xml;
+ g_autoptr(AsApp) app = as_app_new ();
+ g_autoptr(GBytes) bytes = NULL;
+ bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error);
+ if (bytes == NULL)
+ return NULL;
+ as_app_set_id (app, xb_builder_source_ctx_get_filename (ctx));
+ if (!as_app_parse_data (app, bytes, AS_APP_PARSE_FLAG_USE_FALLBACKS, error))
+ return NULL;
+ xml = as_app_to_xml (app, error);
+ if (xml == NULL)
+ return NULL;
+ g_string_prepend (xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+ return g_memory_input_stream_new_from_data (g_string_free (xml, FALSE), -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_app_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);
+ 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,
+ 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 = xb_builder_new ();
+
+ 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);
+
+ /* 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 (self->installation,
+ 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, cancellable, &error_local)) {
+ g_debug ("Failed to add apps from remote ‘%s’; skipping: %s",
+ flatpak_remote_get_name (xremote), error_local->message);
+ }
+ }
+
+ /* 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,
+ error);
+ if (blobfn == NULL)
+ return FALSE;
+ file = g_file_new_for_path (blobfn);
+ g_debug ("ensuring %s", blobfn);
+ self->silo = xb_builder_ensure (builder, file,
+ XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
+ XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+ NULL, error);
+ if (self->silo == NULL)
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_setup (GsFlatpak *self, GCancellable *cancellable, GError **error)
+{
+ /* watch for changes */
+ self->monitor = flatpak_installation_create_monitor (self->installation,
+ 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 AS_APP_STATE_INSTALLING:
+ plugin_status = GS_PLUGIN_STATUS_INSTALLING;
+ break;
+ case AS_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,
+ 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;
+ 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 (self->installation,
+ remote_name,
+ cancellable,
+ &error_local)) {
+ g_debug ("Failed to update metadata for remote %s: %s\n",
+ 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 (self->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, guint cache_age,
+ GCancellable *cancellable, GError **error)
+{
+ gboolean ret;
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ /* get remotes */
+ xremotes = flatpak_installation_list_remotes (self->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;
+ guint 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;
+
+ locker = g_mutex_locker_new (&self->broken_remotes_mutex);
+
+ /* skip known-broken repos */
+ remote_name = flatpak_remote_get_name (xremote);
+ if (g_hash_table_lookup (self->broken_remotes, remote_name) != NULL) {
+ g_debug ("skipping known broken remote: %s", remote_name);
+ continue;
+ }
+
+ /* 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) {
+ g_autofree gchar *fn = g_file_get_path (file_timestamp);
+ g_debug ("%s is only %u seconds old, so ignoring refresh",
+ fn, tmp);
+ continue;
+ }
+
+ /* download new data */
+ g_debug ("%s is %u seconds old, so downloading new data",
+ remote_name, tmp);
+ ret = gs_flatpak_refresh_appstream_remote (self,
+ remote_name,
+ 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);
+ /* 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 */
+ event = gs_plugin_event_new ();
+ gs_flatpak_error_convert (&error_local);
+ gs_plugin_event_set_error (event, error_local);
+ 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, cancellable, error))
+ return FALSE;
+
+ return TRUE;
+}
+
+static void
+gs_flatpak_set_metadata_installed (GsFlatpak *self, GsApp *app,
+ FlatpakInstalledRef *xref)
+{
+#if FLATPAK_CHECK_VERSION(1,1,3)
+ const gchar *appdata_version;
+#endif
+ 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_APP_KIND_RUNTIME is not good enough because it
+ * could be e.g. AS_APP_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_app_set_origin (app, flatpak_installed_ref_get_origin (xref));
+
+ /* this is faster than flatpak_installation_fetch_remote_size_sync() */
+ size_installed = flatpak_installed_ref_get_installed_size (xref);
+ if (size_installed != 0)
+ gs_app_set_size_installed (app, size_installed);
+
+#if FLATPAK_CHECK_VERSION(1,1,3)
+ appdata_version = flatpak_installed_ref_get_appdata_version (xref);
+ if (appdata_version != NULL)
+ gs_app_set_version (app, appdata_version);
+#endif
+}
+
+static GsApp *
+gs_flatpak_create_installed (GsFlatpak *self,
+ FlatpakInstalledRef *xref)
+{
+ 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));
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ gs_flatpak_set_metadata_installed (self, app, xref);
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_flatpak_add_installed (GsFlatpak *self, GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+
+ /* get apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs (self->installation,
+ cancellable, error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ 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);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_sources (GsFlatpak *self, GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ /* refresh */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ /* get installed apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs (self->installation,
+ cancellable,
+ error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* get available remotes */
+ xremotes = flatpak_installation_list_remotes (self->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);
+ gs_app_add_related (app, related);
+ }
+ }
+ return TRUE;
+}
+
+GsApp *
+gs_flatpak_find_source_by_url (GsFlatpak *self,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ g_return_val_if_fail (url != NULL, NULL);
+
+ xremotes = flatpak_installation_list_remotes (self->installation, 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,
+ GCancellable *cancellable, GError **error)
+{
+ g_autoptr(GPtrArray) xremotes = NULL;
+ g_autoptr(GPtrArray) xrefs = NULL;
+
+ g_return_val_if_fail (ref != NULL, NULL);
+
+ /* get all the installed apps (no network I/O) */
+ xrefs = flatpak_installation_list_installed_refs (self->installation,
+ cancellable,
+ error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ for (guint i = 0; i < xrefs->len; i++) {
+ FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i);
+ g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref));
+ if (g_strcmp0 (ref, ref_tmp) == 0)
+ return gs_flatpak_create_installed (self, xref);
+ }
+
+ /* look at each remote xref */
+ xremotes = flatpak_installation_list_remotes (self->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 (self->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);
+ }
+ }
+ }
+
+ /* nothing found */
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "cannot find %s", ref);
+ return NULL;
+}
+
+static FlatpakRemote *
+gs_flatpak_create_new_remote (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *gpg_key;
+ const gchar *branch;
+ g_autoptr(FlatpakRemote) xremote = NULL;
+
+ /* create a new remote */
+ xremote = flatpak_remote_new (gs_app_get_id (app));
+ flatpak_remote_set_url (xremote, gs_flatpak_app_get_repo_url (app));
+ flatpak_remote_set_noenumerate (xremote, FALSE);
+ if (gs_app_get_summary (app) != NULL)
+ flatpak_remote_set_title (xremote, gs_app_get_summary (app));
+
+ /* 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);
+
+ return g_steal_pointer (&xremote);
+}
+
+gboolean
+gs_flatpak_app_install_source (GsFlatpak *self, GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+
+ xremote = flatpak_installation_get_remote_by_name (self->installation,
+ gs_app_get_id (app),
+ cancellable, NULL);
+ if (xremote != NULL) {
+ /* if the remote already exists, just enable it */
+ g_debug ("enabling existing remote %s", flatpak_remote_get_name (xremote));
+ flatpak_remote_set_disabled (xremote, FALSE);
+ } else {
+ /* create a new remote */
+ xremote = gs_flatpak_create_new_remote (self, app, cancellable, error);
+ }
+
+ /* install it */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ if (!flatpak_installation_modify_remote (self->installation,
+ xremote,
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ g_prefix_error (error, "cannot modify remote: ");
+ gs_app_set_state_recover (app);
+ return FALSE;
+ }
+
+ /* invalidate cache */
+ g_rw_lock_reader_lock (&self->silo_lock);
+ if (self->silo != NULL)
+ xb_silo_invalidate (self->silo);
+ g_rw_lock_reader_unlock (&self->silo_lock);
+
+ /* success */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ return TRUE;
+}
+
+static GsApp *
+get_main_app_of_related (GsFlatpak *self,
+ GsApp *related_app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakInstalledRef) ref = NULL;
+ const gchar *ref_name;
+ g_auto(GStrv) app_tokens = NULL;
+
+ 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;
+ }
+
+ /* 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 (self->installation,
+ FLATPAK_REF_KIND_APP,
+ app_tokens[1],
+ app_tokens[2],
+ app_tokens[3],
+ cancellable,
+ error);
+ if (ref == NULL)
+ return NULL;
+
+ return gs_flatpak_create_installed (self, ref);
+}
+
+static GsApp *
+get_real_app_for_update (GsFlatpak *self,
+ GsApp *app,
+ 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, 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, AS_APP_STATE_UPDATABLE_LIVE);
+ }
+
+ return main_app;
+}
+
+gboolean
+gs_flatpak_add_updates (GsFlatpak *self, GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) xrefs = NULL;
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ /* get all the updatable apps and runtimes */
+ xrefs = flatpak_installation_list_installed_refs_for_update (self->installation,
+ cancellable,
+ error);
+ if (xrefs == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* 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);
+ if (latest_commit == NULL) {
+ g_debug ("could not get latest commit for %s",
+ flatpak_ref_get_name (FLATPAK_REF (xref)));
+ continue;
+ }
+
+ app = gs_flatpak_create_installed (self, xref);
+ main_app = get_real_app_for_update (self, app, 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) != AS_APP_STATE_INSTALLING)
+ gs_app_set_state (main_app, AS_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) != AS_APP_STATE_INSTALLING)
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+
+ /* already downloaded */
+ if (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 (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, 0);
+ gs_app_list_add (list, main_app);
+
+ /* 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) == 0) {
+ if (!flatpak_installation_fetch_remote_size_sync (self->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_APP_SIZE_UNKNOWABLE);
+ } else {
+ gs_app_set_size_download (main_app, download_size);
+ }
+ }
+ }
+ gs_flatpak_set_update_permissions (self, main_app, xref);
+ gs_app_list_add (list, main_app);
+ }
+
+ /* success */
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_refresh (GsFlatpak *self,
+ guint cache_age,
+ 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 */
+ if (!flatpak_installation_drop_caches (self->installation,
+ 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 */
+ g_rw_lock_reader_lock (&self->silo_lock);
+ if (self->silo != NULL)
+ xb_silo_invalidate (self->silo);
+ g_rw_lock_reader_unlock (&self->silo_lock);
+
+ /* update AppStream metadata */
+ if (!gs_flatpak_refresh_appstream (self, cache_age, cancellable, error))
+ return FALSE;
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_refine_item_origin_hostname (GsFlatpak *self, GsApp *app,
+ 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 (self->installation,
+ 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,
+ GCancellable *cancellable,
+ 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_APP_KIND_SOURCE)
+ 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *ref_display = NULL;
+ g_autoptr(GPtrArray) xremotes = NULL;
+
+ /* already set */
+ if (gs_app_get_origin (app) != NULL)
+ return TRUE;
+
+ /* not applicable */
+ if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL)
+ return TRUE;
+
+ /* ensure metadata exists */
+ if (!gs_refine_item_metadata (self, app, cancellable, 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 (self->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 (self->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_app_set_origin (app, remote_name);
+ 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_appstream_store,
+ * in order to avoid taking the writer lock on self->silo_lock */
+static gboolean
+gs_flatpak_refine_app_state_unlocked (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakInstalledRef) ref = NULL;
+ g_autoptr(GPtrArray) installed_refs = NULL;
+
+ /* already found */
+ if (gs_app_get_state (app) != AS_APP_STATE_UNKNOWN)
+ return TRUE;
+
+ /* need broken out metadata */
+ if (!gs_refine_item_metadata (self, app, 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 (self->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);
+ g_mutex_unlock (&self->installed_refs_mutex);
+
+ 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;
+ }
+ }
+ 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);
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_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;
+ }
+
+ /* ensure origin set */
+ if (!gs_plugin_refine_item_origin (self, app, cancellable, error))
+ return FALSE;
+
+ /* anything not installed just check the remote is still present */
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN &&
+ gs_app_get_origin (app) != NULL) {
+ g_autoptr(FlatpakRemote) xremote = NULL;
+ xremote = flatpak_installation_get_remote_by_name (self->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, AS_APP_STATE_UNAVAILABLE);
+ } else {
+ g_debug ("marking %s as available with flatpak",
+ gs_app_get_unique_id (app));
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ }
+ } else {
+ gs_app_set_state (app, AS_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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* ensure valid */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ return gs_flatpak_refine_app_state_unlocked (self, app, cancellable, error);
+}
+
+static GsApp *
+gs_flatpak_create_runtime (GsFlatpak *self, GsApp *parent, const gchar *runtime)
+{
+ g_autofree gchar *source = NULL;
+ g_auto(GStrv) split = NULL;
+ g_autoptr(GsApp) app_cache = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* 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_APP_KIND_RUNTIME);
+ gs_app_set_branch (app, split[2]);
+
+ /* search in the cache */
+ app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cache != NULL) {
+ /* 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);
+ }
+
+ /* if the app is per-user we can also use the installed system runtime */
+ if (gs_app_get_scope (parent) == AS_APP_SCOPE_USER) {
+ gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN);
+ app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app));
+ if (app_cache != NULL)
+ return g_steal_pointer (&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]);
+
+ /* 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,
+ 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_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;
+ }
+
+ gs_app_set_permissions (app, perms_from_metadata (kf));
+ /* 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);
+ 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GBytes) data = NULL;
+ g_autoptr(FlatpakRef) xref = 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 (self->installation,
+ gs_app_get_origin (app),
+ xref,
+ cancellable,
+ error);
+ if (data == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+ return g_steal_pointer (&data);
+}
+
+static gboolean
+gs_plugin_refine_item_metadata (GsFlatpak *self,
+ GsApp *app,
+ 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_APP_KIND_SOURCE)
+ 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);
+ 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, 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, error))
+ return FALSE;
+ return TRUE;
+}
+
+static FlatpakInstalledRef *
+gs_flatpak_get_installed_ref (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ FlatpakInstalledRef *ref;
+ ref = flatpak_installation_get_installed_ref (self->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 (ref == NULL)
+ gs_flatpak_error_convert (error);
+ return ref;
+}
+
+static gboolean
+gs_plugin_refine_item_size (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret;
+ guint64 download_size = GS_APP_SIZE_UNKNOWABLE;
+ guint64 installed_size = GS_APP_SIZE_UNKNOWABLE;
+
+ /* not applicable */
+ if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL)
+ return TRUE;
+ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE)
+ 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) > 0)
+ return TRUE;
+ } else {
+ if (gs_app_get_size_installed (app) > 0 &&
+ gs_app_get_size_download (app) > 0)
+ return TRUE;
+ }
+
+ /* need runtime */
+ if (!gs_plugin_refine_item_metadata (self, app, cancellable, error))
+ return FALSE;
+
+ /* calculate the platform size too if the app is not installed */
+ if (gs_app_get_state (app) == AS_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,
+ cancellable,
+ error))
+ return FALSE;
+ if (gs_app_get_state (app_runtime) == AS_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,
+ cancellable,
+ error))
+ return FALSE;
+ }
+ }
+
+ /* just get the size of the app */
+ if (!gs_plugin_refine_item_origin (self, app,
+ 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, cancellable, error);
+ if (xref == NULL)
+ return FALSE;
+ installed_size = flatpak_installed_ref_get_installed_size (xref);
+ if (installed_size == 0)
+ installed_size = GS_APP_SIZE_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 (self->installation,
+ gs_app_get_origin (app),
+ xref,
+ &download_size,
+ &installed_size,
+ cancellable,
+ &error_local);
+
+ if (!ret) {
+ g_warning ("libflatpak failed to return application "
+ "size: %s", error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ gs_app_set_size_installed (app, installed_size);
+ gs_app_set_size_download (app, 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 AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_AVAILABLE:
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ gs_app_set_version (app, version);
+ break;
+ case AS_APP_STATE_UPDATABLE:
+ case AS_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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *const *locales = g_get_language_names ();
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(XbBuilder) builder = xb_builder_new ();
+ 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;
+
+ /* 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_app_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);
+ silo = xb_builder_compile (builder,
+ XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
+ cancellable,
+ error);
+ 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;
+
+ /* 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 gboolean
+gs_flatpak_refine_appstream (GsFlatpak *self,
+ GsApp *app,
+ XbSilo *silo,
+ GsPluginRefineFlags flags,
+ 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 || gs_flatpak_app_get_ref_name (app) == 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 (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);
+ /* 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 (self->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),
+ NULL, NULL);
+ if (installed_ref == NULL)
+ return TRUE; /* the app may not be installed */
+
+#if FLATPAK_CHECK_VERSION(1,1,2)
+ appstream_gz = flatpak_installed_ref_load_appdata (installed_ref, NULL, NULL);
+#endif
+ if (appstream_gz == NULL)
+ return TRUE;
+
+ 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,
+ 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;
+}
+
+/* the _unlocked() version doesn't call gs_flatpak_rescan_appstream_store,
+ * in order to avoid taking the writer lock on self->silo_lock */
+static gboolean
+gs_flatpak_refine_app_unlocked (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ AsAppState old_state = gs_app_get_state (app);
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ /* not us */
+ if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK)
+ return TRUE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ /* always do AppStream properties */
+ if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, cancellable, error))
+ return FALSE;
+
+ /* AppStream sets the source to appname/arch/branch */
+ if (!gs_refine_item_metadata (self, app, cancellable, error)) {
+ g_prefix_error (error, "failed to get metadata: ");
+ return FALSE;
+ }
+
+ /* check the installed state */
+ if (!gs_flatpak_refine_app_state_unlocked (self, app, cancellable, error)) {
+ g_prefix_error (error, "failed to get state: ");
+ return FALSE;
+ }
+
+ /* scope is fast, do unconditionally */
+ if (gs_app_get_state (app) != AS_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, 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,
+ 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 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;
+ }
+ }
+ }
+
+ /* origin-hostname */
+ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) {
+ if (!gs_plugin_refine_item_origin_hostname (self, app,
+ 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,
+ 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 get permissions: ");
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_refine_app (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* ensure valid */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ return gs_flatpak_refine_app_unlocked (self, app, flags, cancellable, error);
+}
+
+gboolean
+gs_flatpak_refine_wildcard (GsFlatpak *self, GsApp *app,
+ GsAppList *list, GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable, GError **error)
+{
+ const gchar *id;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ /* not enough info to find */
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ return TRUE;
+
+ /* ensure valid */
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+
+ /* 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;
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) new = NULL;
+ new = 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, 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* launch the app */
+ if (!flatpak_installation_launch (self->installation,
+ 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakRemote) xremote = NULL;
+
+ /* find the remote */
+ xremote = flatpak_installation_get_remote_by_name (self->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, AS_APP_STATE_REMOVING);
+ if (!flatpak_installation_remove_remote (self->installation,
+ gs_app_get_id (app),
+ cancellable,
+ error)) {
+ gs_flatpak_error_convert (error);
+ gs_app_set_state_recover (app);
+ return FALSE;
+ }
+
+ /* invalidate cache */
+ g_rw_lock_reader_lock (&self->silo_lock);
+ if (self->silo != NULL)
+ xb_silo_invalidate (self->silo);
+ g_rw_lock_reader_unlock (&self->silo_lock);
+
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ return TRUE;
+}
+
+GsApp *
+gs_flatpak_file_to_app_bundle (GsFlatpak *self,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint size;
+ g_autoptr(GBytes) appstream_gz = NULL;
+ g_autoptr(GBytes) icon_data = NULL;
+ g_autoptr(GBytes) metadata = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(FlatpakBundleRef) xref_bundle = NULL;
+ g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+ const char *origin = 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;
+ }
+
+ /* get the origin if it's already installed */
+ installed_ref = flatpak_installation_get_installed_ref (self->installation,
+ flatpak_ref_get_kind (FLATPAK_REF (xref_bundle)),
+ flatpak_ref_get_name (FLATPAK_REF (xref_bundle)),
+ flatpak_ref_get_arch (FLATPAK_REF (xref_bundle)),
+ flatpak_ref_get_branch (FLATPAK_REF (xref_bundle)),
+ NULL, NULL);
+ if (installed_ref != NULL)
+ origin = flatpak_installed_ref_get_origin (installed_ref);
+
+ /* load metadata */
+ app = gs_flatpak_create_app (self, origin, FLATPAK_REF (xref_bundle));
+ if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) {
+ if (gs_flatpak_app_get_ref_name (app) == NULL)
+ gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref_bundle));
+ return g_steal_pointer (&app);
+ }
+ gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_BUNDLE);
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL);
+ gs_app_set_size_installed (app, 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),
+ 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, origin, installed_ref,
+ appstream_gz,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ 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 icon */
+ size = 64 * (gint) gs_plugin_get_scale (self->plugin);
+ icon_data = flatpak_bundle_ref_get_icon (xref_bundle, size);
+ if (icon_data == NULL)
+ icon_data = flatpak_bundle_ref_get_icon (xref_bundle, 64);
+ if (icon_data != NULL) {
+ g_autoptr(GInputStream) stream_icon = NULL;
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+ stream_icon = g_memory_input_stream_new_from_bytes (icon_data);
+ pixbuf = gdk_pixbuf_new_from_stream (stream_icon, cancellable, error);
+ if (pixbuf == NULL) {
+ gs_utils_error_convert_gdk_pixbuf (error);
+ return NULL;
+ }
+ gs_app_set_pixbuf (app, pixbuf);
+ } else {
+ g_autoptr(AsIcon) icon = NULL;
+ icon = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
+ as_icon_set_name (icon, "application-x-executable");
+ 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);
+}
+
+GsApp *
+gs_flatpak_file_to_app_ref (GsFlatpak *self,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsApp *runtime;
+ const gchar *const *locales = g_get_language_names ();
+ const gchar *remote_name;
+ gsize len = 0;
+ g_autofree gchar *contents = NULL;
+ g_autoptr(FlatpakRemoteRef) xref = 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;
+
+ /* 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 */
+ ref_name = g_key_file_get_string (kf, "Flatpak Ref", "Name", error);
+ if (ref_name == NULL) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+
+ /* install the remote, but not the app */
+ ref_file_data = g_bytes_new (contents, len);
+ xref = flatpak_installation_install_ref_file (self->installation,
+ ref_file_data,
+ cancellable,
+ error);
+ if (xref == NULL) {
+ gs_flatpak_error_convert (error);
+ return NULL;
+ }
+
+ /* load metadata */
+ app = gs_flatpak_create_app (self, NULL /* origin */, FLATPAK_REF (xref));
+ if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) {
+ if (gs_flatpak_app_get_ref_name (app) == NULL)
+ gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref));
+ return g_steal_pointer (&app);
+ }
+ 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, AS_APP_STATE_AVAILABLE_LOCAL);
+ gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref));
+
+ /* 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_autoptr(AsIcon) ic = as_icon_new ();
+ as_icon_set_kind (ic, AS_ICON_KIND_REMOTE);
+ as_icon_set_url (ic, ref_icon);
+ gs_app_add_icon (app, ic);
+ }
+
+ /* set the origin data */
+ remote_name = flatpak_remote_ref_get_remote_name (xref);
+ g_debug ("auto-created remote name: %s", remote_name);
+ xremote = flatpak_installation_get_remote_by_name (self->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 (app, remote_name);
+ 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,
+ cancellable, &error_local)) {
+ g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+ gs_flatpak_error_convert (&error_local);
+ gs_plugin_event_set_app (event, app);
+ gs_plugin_event_set_error (event, error_local);
+ 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, cancellable, error))
+ return NULL;
+
+ /* the new runtime is available from the RuntimeRepo */
+ runtime = gs_app_get_runtime (app);
+ if (runtime != NULL && gs_app_get_state (runtime) == AS_APP_STATE_UNKNOWN) {
+ g_autofree gchar *uri = NULL;
+ uri = g_key_file_get_string (kf, "Flatpak Ref", "RuntimeRepo", NULL);
+ gs_flatpak_app_set_runtime_url (runtime, uri);
+ }
+
+ /* parse it */
+ if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, cancellable, error))
+ return NULL;
+
+ /* build silo */
+ 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,
+ G_MAXUINT64,
+ cancellable,
+ error))
+ return NULL;
+
+ /* success */
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_flatpak_search (GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ 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 (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ if (!gs_appstream_search (self->plugin, self->silo, values, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, list_tmp);
+ 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 (self->installation,
+ kind,
+ split[1],
+ split[2],
+ split[3],
+ NULL, NULL);
+ if (installed_ref == NULL) {
+ g_ptr_array_add (silos_to_remove, 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);
+ 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ return gs_appstream_add_category_apps (self->plugin, self->silo,
+ category, list,
+ cancellable, error);
+}
+
+gboolean
+gs_flatpak_add_categories (GsFlatpak *self,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ return gs_appstream_add_categories (self->plugin, self->silo,
+ list, cancellable, error);
+}
+
+gboolean
+gs_flatpak_add_popular (GsFlatpak *self,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ if (!gs_appstream_add_popular (self->plugin, 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ if (!gs_appstream_add_featured (self->plugin, self->silo, list_tmp,
+ cancellable, error))
+ return FALSE;
+
+ gs_app_list_add_list (list, list_tmp);
+
+ return TRUE;
+}
+
+gboolean
+gs_flatpak_add_alternates (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ if (!gs_appstream_add_alternates (self->plugin, 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,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+ if (!gs_flatpak_rescan_appstream_store (self, cancellable, error))
+ return FALSE;
+
+ locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+ if (!gs_appstream_add_recent (self->plugin, self->silo, list_tmp, age,
+ cancellable, error))
+ return FALSE;
+
+ gs_flatpak_claim_app_list (self, list_tmp);
+ 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_app_scope_to_string (self->scope));
+ if (flatpak_installation_get_id (self->installation) != NULL) {
+ g_string_append_printf (str, "-%s",
+ flatpak_installation_get_id (self->installation));
+ }
+ if (self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY)
+ g_string_append (str, "-temp");
+ self->id = g_string_free (str, FALSE);
+ }
+ return self->id;
+}
+
+AsAppScope
+gs_flatpak_get_scope (GsFlatpak *self)
+{
+ return self->scope;
+}
+
+FlatpakInstallation *
+gs_flatpak_get_installation (GsFlatpak *self)
+{
+ return self->installation;
+}
+
+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);
+
+ g_free (self->id);
+ g_object_unref (self->installation);
+ 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_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);
+}
+
+GsFlatpak *
+gs_flatpak_new (GsPlugin *plugin, FlatpakInstallation *installation, GsFlatpakFlags flags)
+{
+ GsFlatpak *self;
+ self = g_object_new (GS_TYPE_FLATPAK, NULL);
+ self->installation = g_object_ref (installation);
+ self->scope = flatpak_installation_get_is_user (installation)
+ ? AS_APP_SCOPE_USER : AS_APP_SCOPE_SYSTEM;
+ self->plugin = g_object_ref (plugin);
+ self->flags = flags;
+ return GS_FLATPAK (self);
+}
diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h
new file mode 100644
index 0000000..e5af289
--- /dev/null
+++ b/plugins/flatpak/gs-flatpak.h
@@ -0,0 +1,128 @@
+/* -*- 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,
+ /*< private >*/
+ GS_FLATPAK_FLAG_LAST
+} GsFlatpakFlags;
+
+GsFlatpak *gs_flatpak_new (GsPlugin *plugin,
+ FlatpakInstallation *installation,
+ GsFlatpakFlags flags);
+FlatpakInstallation *gs_flatpak_get_installation (GsFlatpak *self);
+
+GsApp *gs_flatpak_ref_to_app (GsFlatpak *self, const gchar *ref, GCancellable *cancellable, GError **error);
+
+AsAppScope 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,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_sources (GsFlatpak *self,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_updates (GsFlatpak *self,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refresh (GsFlatpak *self,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_app (GsFlatpak *self,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_app_state (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_refine_wildcard (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_launch (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_app_remove_source (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_app_install_source (GsFlatpak *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_file_to_app_ref (GsFlatpak *self,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_file_to_app_bundle (GsFlatpak *self,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_flatpak_find_source_by_url (GsFlatpak *self,
+ const gchar *name,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_search (GsFlatpak *self,
+ const gchar * const *values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_categories (GsFlatpak *self,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_category_apps (GsFlatpak *self,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_popular (GsFlatpak *self,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_featured (GsFlatpak *self,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_alternates (GsFlatpak *self,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_flatpak_add_recent (GsFlatpak *self,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error);
+
+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..5b7a549
--- /dev/null
+++ b/plugins/flatpak/gs-plugin-flatpak.c
@@ -0,0 +1,1320 @@
+/* -*- 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+
+ */
+
+/* Notes:
+ *
+ * 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
+ */
+
+#include <config.h>
+
+#include <flatpak.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"
+
+struct GsPluginData {
+ GPtrArray *flatpaks; /* of GsFlatpak */
+ gboolean has_system_helper;
+ const gchar *destdir_for_tests;
+};
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+ const gchar *action_id = "org.freedesktop.Flatpak.appstream-update";
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPermission) permission = NULL;
+
+ priv->flatpaks = 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");
+
+ /* set name of MetaInfo file */
+ gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Flatpak");
+
+ /* 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, &error_local);
+ if (permission == NULL) {
+ g_debug ("no permission for %s: %s", action_id, error_local->message);
+ g_clear_error (&error_local);
+ } else {
+ priv->has_system_helper = g_permission_get_allowed (permission) ||
+ g_permission_get_can_acquire (permission);
+ }
+
+ /* used for self tests */
+ priv->destdir_for_tests = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR");
+}
+
+static gboolean
+_as_app_scope_is_compatible (AsAppScope scope1, AsAppScope scope2)
+{
+ if (scope1 == AS_APP_SCOPE_UNKNOWN)
+ return TRUE;
+ if (scope2 == AS_APP_SCOPE_UNKNOWN)
+ return TRUE;
+ return scope1 == scope2;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_ptr_array_unref (priv->flatpaks);
+}
+
+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, gs_plugin_get_name (plugin));
+}
+
+static gboolean
+gs_plugin_flatpak_add_installation (GsPlugin *plugin,
+ FlatpakInstallation *installation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GsFlatpak) flatpak = NULL;
+
+ /* create and set up */
+ flatpak = gs_flatpak_new (plugin, 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 (priv->flatpaks, g_steal_pointer (&flatpak));
+ return TRUE;
+}
+
+gboolean
+gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* clear in case we're called from resetup in the self tests */
+ g_ptr_array_set_size (priv->flatpaks, 0);
+
+ /* we use a permissions helper to elevate privs */
+ if (priv->has_system_helper && priv->destdir_for_tests == NULL) {
+ g_autoptr(GPtrArray) installations = NULL;
+ installations = flatpak_get_system_installations (cancellable, error);
+ if (installations == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < installations->len; i++) {
+ FlatpakInstallation *installation = g_ptr_array_index (installations, i);
+ if (!gs_plugin_flatpak_add_installation (plugin, installation,
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* in gs-self-test */
+ if (priv->destdir_for_tests != NULL) {
+ g_autofree gchar *full_path = g_build_filename (priv->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);
+ if (installation == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_plugin_flatpak_add_installation (plugin, installation,
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+
+ /* per-user installations always available when not in self tests */
+ if (priv->destdir_for_tests == NULL) {
+ g_autoptr(FlatpakInstallation) installation = NULL;
+ installation = flatpak_installation_new_user (cancellable, error);
+ if (installation == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_plugin_flatpak_add_installation (plugin, installation,
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_installed (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_sources (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_sources (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_updates (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_updates (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_refresh (flatpak, cache_age, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static GsFlatpak *
+gs_plugin_flatpak_get_handler (GsPlugin *plugin, GsApp *app)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ const gchar *object_id;
+
+ /* only process this app if was created by this plugin */
+ if (g_strcmp0 (gs_app_get_management_plugin (app),
+ gs_plugin_get_name (plugin)) != 0) {
+ return NULL;
+ }
+
+ /* specified an explicit name */
+ object_id = gs_flatpak_app_get_object_id (app);
+ if (object_id != NULL) {
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (g_strcmp0 (gs_flatpak_get_id (flatpak), object_id) == 0)
+ return flatpak;
+ }
+ }
+
+ /* find a scope that matches */
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (_as_app_scope_is_compatible (gs_flatpak_get_scope (flatpak),
+ gs_app_get_scope (app)))
+ return flatpak;
+ }
+ return NULL;
+}
+
+static gboolean
+gs_plugin_flatpak_refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ 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_APP_SCOPE_UNKNOWN) {
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i);
+ g_autoptr(GError) error_local = NULL;
+ if (gs_flatpak_refine_app_state (flatpak_tmp, app,
+ cancellable, &error_local)) {
+ flatpak = flatpak_tmp;
+ break;
+ } else {
+ g_debug ("%s", error_local->message);
+ }
+ }
+ } else {
+ flatpak = gs_plugin_flatpak_get_handler (plugin, app);
+ }
+ if (flatpak == NULL)
+ return TRUE;
+ return gs_flatpak_refine_app (flatpak, app, flags, cancellable, error);
+}
+
+
+static gboolean
+refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* only process this app if was created by this plugin */
+ if (g_strcmp0 (gs_app_get_management_plugin (app),
+ gs_plugin_get_name (plugin)) != 0) {
+ return TRUE;
+ }
+
+ /* get the runtime first */
+ if (!gs_plugin_flatpak_refine_app (plugin, app, flags, 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 (plugin, app,
+ flags,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (!refine_app (plugin, app, flags, cancellable, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refine_wildcard (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_refine_wildcard (flatpak, app, list, flags,
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_launch (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (plugin, app);
+ if (flatpak == NULL)
+ return TRUE;
+ return gs_flatpak_launch (flatpak, app, cancellable, error);
+}
+
+/* ref full */
+static GsApp *
+gs_plugin_flatpak_find_app_by_ref (GsPlugin *plugin, const gchar *ref,
+ GCancellable *cancellable, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_debug ("finding ref %s", ref);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak_tmp = g_ptr_array_index (priv->flatpaks, i);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ app = gs_flatpak_ref_to_app (flatpak_tmp, ref, 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, GsPlugin *plugin)
+{
+ 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 (plugin), NULL);
+
+ /* search through each GsFlatpak */
+ return gs_plugin_flatpak_find_app_by_ref (plugin, ref, NULL, NULL);
+}
+
+/*
+ * 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 (GsPlugin *plugin,
+ 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 */
+ 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 (plugin, app);
+ if (flatpak != NULL) {
+ GsAppList *list_tmp = g_hash_table_lookup (applist_by_flatpaks, flatpak);
+ 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);
+ }
+ }
+
+ return g_steal_pointer (&applist_by_flatpaks);
+}
+
+#if FLATPAK_CHECK_VERSION(1,6,0)
+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 (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE))
+ 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 (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE))
+ 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);
+
+ event = gs_plugin_event_new ();
+ gs_flatpak_error_convert (&error_local);
+ gs_plugin_event_set_error (event, error_local);
+ 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);
+
+ event = gs_plugin_event_new ();
+ gs_flatpak_error_convert (&error_local);
+ gs_plugin_event_set_error (event, error_local);
+ 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");
+}
+#endif
+
+static FlatpakTransaction *
+_build_transaction (GsPlugin *plugin, GsFlatpak *flatpak,
+ GCancellable *cancellable, GError **error)
+{
+ FlatpakInstallation *installation;
+#if !FLATPAK_CHECK_VERSION(1, 7, 3)
+ g_autoptr(GFile) installation_path = NULL;
+#endif /* flatpak < 1.7.3 */
+ g_autoptr(FlatpakInstallation) installation_clone = NULL;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+
+ installation = gs_flatpak_get_installation (flatpak);
+
+#if !FLATPAK_CHECK_VERSION(1, 7, 3)
+ /* Operate on a copy of the installation so we can set the interactive
+ * flag for the duration of this transaction. */
+ installation_path = flatpak_installation_get_path (installation);
+ installation_clone = flatpak_installation_new_for_path (installation_path,
+ flatpak_installation_get_is_user (installation),
+ cancellable, error);
+ if (installation_clone == NULL)
+ return NULL;
+
+ /* Let flatpak know if it is a background operation */
+ flatpak_installation_set_no_interaction (installation_clone,
+ !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE));
+#else /* if flatpak ≥ 1.7.3 */
+ installation_clone = g_object_ref (installation);
+#endif /* flatpak ≥ 1.7.3 */
+
+ /* 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;
+ }
+
+#if FLATPAK_CHECK_VERSION(1, 7, 3)
+ /* Let flatpak know if it is a background operation */
+ flatpak_transaction_set_no_interaction (transaction,
+ !gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE));
+#endif /* flatpak ≥ 1.7.3 */
+
+ /* connect up signals */
+ g_signal_connect (transaction, "ref-to-app",
+ G_CALLBACK (_ref_to_app), plugin);
+#if FLATPAK_CHECK_VERSION(1,6,0)
+ 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);
+#endif
+
+ /* use system installations as dependency sources for user installations */
+ flatpak_transaction_add_default_dependency_sources (transaction);
+
+ return g_steal_pointer (&transaction);
+}
+
+gboolean
+gs_plugin_download (GsPlugin *plugin, GsAppList *list,
+ GCancellable *cancellable, GError **error)
+{
+ g_autoptr(GHashTable) applist_by_flatpaks = NULL;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* build and run transaction for each flatpak installation */
+ applist_by_flatpaks = _group_apps_by_installation (plugin, 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;
+
+ g_assert (GS_IS_FLATPAK (flatpak));
+ g_assert (list_tmp != NULL);
+ g_assert (gs_app_list_length (list_tmp) > 0);
+
+ if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) {
+ g_autoptr(GError) error_local = NULL;
+
+ if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, 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, cancellable, error);
+ if (transaction == NULL) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+#if !FLATPAK_CHECK_VERSION(1,5,1)
+ gs_flatpak_transaction_set_no_deploy (transaction, TRUE);
+#else
+ flatpak_transaction_set_no_deploy (transaction, TRUE);
+#endif
+ 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 (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+ if (!gs_flatpak_transaction_run (transaction, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+
+ /* 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;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsFlatpak *flatpak;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ g_autofree gchar *ref = NULL;
+
+ /* not supported */
+ flatpak = gs_plugin_flatpak_get_handler (plugin, app);
+ if (flatpak == NULL)
+ return TRUE;
+
+ /* is a source */
+ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE)
+ return gs_flatpak_app_remove_source (flatpak, app, cancellable, error);
+
+ /* build and run transaction */
+ transaction = _build_transaction (plugin, flatpak, 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;
+ }
+
+ /* run transaction */
+ gs_app_set_state (app, AS_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 */
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_flatpak_refine_app (flatpak, app,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, error)) {
+ g_prefix_error (error, "failed to run refine for %s: ", ref);
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+app_has_local_source (GsApp *app)
+{
+ const gchar *url = gs_app_get_origin_hostname (app);
+ return url != NULL && g_str_has_prefix (url, "file://");
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ GsFlatpak *flatpak;
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+
+ /* 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, AS_APP_STATE_QUEUED_FOR_INSTALL);
+ return TRUE;
+ }
+
+ /* set the app scope */
+ if (gs_app_get_scope (app) == AS_APP_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_APP_SCOPE_SYSTEM : AS_APP_SCOPE_USER);
+ if (!priv->has_system_helper) {
+ g_info ("no flatpak system helper is available, using user");
+ gs_app_set_scope (app, AS_APP_SCOPE_USER);
+ }
+ if (priv->destdir_for_tests != NULL) {
+ g_debug ("in self tests, using user");
+ gs_app_set_scope (app, AS_APP_SCOPE_USER);
+ }
+ }
+
+ /* not supported */
+ flatpak = gs_plugin_flatpak_get_handler (plugin, app);
+ if (flatpak == NULL)
+ return TRUE;
+
+ /* is a source */
+ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE)
+ return gs_flatpak_app_install_source (flatpak, app, cancellable, error);
+
+ if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) {
+ g_autoptr(GError) error_local = NULL;
+
+ /* 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, cancellable, &error_local)) {
+ g_warning ("Failed to block on download scheduler: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ /* build */
+ transaction = _build_transaction (plugin, flatpak, 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)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ }
+
+ /* run transaction */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ 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 */
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, cancellable, error)) {
+ gs_flatpak_error_convert (error);
+ return FALSE;
+ }
+ if (!gs_flatpak_refine_app (flatpak, app,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ 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;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_flatpak_update (GsPlugin *plugin,
+ GsFlatpak *flatpak,
+ GsAppList *list_tmp,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(FlatpakTransaction) transaction = NULL;
+ gboolean is_update_downloaded = TRUE;
+
+ /* build and run transaction */
+ transaction = _build_transaction (plugin, flatpak, 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;
+
+ ref = gs_flatpak_app_get_ref_display (app);
+ if (!flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) {
+ 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);
+ }
+
+ /* 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, AS_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);
+
+ 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);
+ return FALSE;
+ }
+ gs_plugin_updates_changed (plugin);
+
+ /* get any new state */
+ if (!gs_flatpak_refresh (flatpak, G_MAXUINT, 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,
+ 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)
+{
+ g_autoptr(GHashTable) applist_by_flatpaks = NULL;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* build and run transaction for each flatpak installation */
+ applist_by_flatpaks = _group_apps_by_installation (plugin, 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_assert (GS_IS_FLATPAK (flatpak));
+ g_assert (list_tmp != NULL);
+ g_assert (gs_app_list_length (list_tmp) > 0);
+
+ if (!gs_plugin_flatpak_update (plugin, flatpak, list_tmp, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_repo (GsPlugin *plugin,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ 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 < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, 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),
+ cancellable, &error_local);
+ if (app_tmp == NULL) {
+ g_debug ("%s", error_local->message);
+ continue;
+ }
+ return g_steal_pointer (&app_tmp);
+ }
+
+ /* this is new */
+ gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
+ return g_steal_pointer (&app);
+}
+
+static GsFlatpak *
+gs_plugin_flatpak_create_temporary (GsPlugin *plugin, 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,
+ 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 (plugin, installation, GS_FLATPAK_FLAG_IS_TEMPORARY);
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_bundle (GsPlugin *plugin,
+ GFile *file,
+ 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 (plugin, cancellable, error);
+ if (flatpak_tmp == NULL)
+ return NULL;
+
+ /* add object */
+ app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, 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 (plugin, ref, cancellable, NULL);
+ if (app_tmp != NULL)
+ return g_steal_pointer (&app_tmp);
+
+ /* force this to be 'any' scope for installation */
+ gs_app_set_scope (app, AS_APP_SCOPE_UNKNOWN);
+
+ /* this is new */
+ return g_steal_pointer (&app);
+}
+
+static GsApp *
+gs_plugin_flatpak_file_to_app_ref (GsPlugin *plugin,
+ GFile *file,
+ 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 (plugin, cancellable, error);
+ if (flatpak_tmp == NULL)
+ return NULL;
+
+ /* add object */
+ app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, 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 (plugin, ref, cancellable, NULL);
+ if (app_tmp != NULL)
+ return g_steal_pointer (&app_tmp);
+
+ /* force this to be 'any' scope for installation */
+ gs_app_set_scope (app, AS_APP_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 (plugin,
+ runtime_ref,
+ 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, AS_APP_STATE_AVAILABLE_LOCAL);
+ }
+ }
+
+ /* this is new */
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_plugin_file_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *content_type = NULL;
+ g_autoptr(GsApp) app = NULL;
+ 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 (plugin, file,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ } else if (g_strv_contains (mimetypes_repo, content_type)) {
+ app = gs_plugin_flatpak_file_to_app_repo (plugin, file,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ } else if (g_strv_contains (mimetypes_ref, content_type)) {
+ app = gs_plugin_flatpak_file_to_app_ref (plugin, file,
+ cancellable, error);
+ if (app == NULL)
+ return FALSE;
+ }
+ if (app != NULL)
+ gs_app_list_add (list, app);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_search (GsPlugin *plugin,
+ gchar **values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_search (flatpak, (const gchar * const *) values, list,
+ cancellable, error)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_categories (GsPlugin *plugin,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_categories (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_category_apps (GsPlugin *plugin,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_category_apps (flatpak,
+ category,
+ list,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_popular (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_popular (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_alternates (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_alternates (flatpak, app, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_featured (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_featured (flatpak, list, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_recent (GsPlugin *plugin,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ for (guint i = 0; i < priv->flatpaks->len; i++) {
+ GsFlatpak *flatpak = g_ptr_array_index (priv->flatpaks, i);
+ if (!gs_flatpak_add_recent (flatpak, list, age, cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/plugins/flatpak/gs-self-test.c b/plugins/flatpak/gs-self-test.c
new file mode 100644
index 0000000..ae0acda
--- /dev/null
+++ b/plugins/flatpak/gs-self-test.c
@@ -0,0 +1,1936 @@
+/* -*- 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"
+
+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), ==, "*/*/*/source/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;
+
+ /* 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_APP_KIND_SOURCE);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "example");
+ g_assert_cmpstr (gs_app_get_management_plugin (app), ==, "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);
+ g_assert_true (gs_app_get_pixbuf (app) != NULL);
+
+ /* now install the remote */
+ 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), ==, AS_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), ==, AS_APP_STATE_INSTALLED);
+
+ /* remove it */
+ 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), ==, AS_APP_STATE_AVAILABLE);
+ 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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_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_APP_KIND_SOURCE);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) G_MAXUINT,
+ NULL);
+ 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);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "flatpak",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "Bingo",
+ "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,
+ NULL);
+ 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_APP_KIND_DESKTOP);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_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 (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/runtime/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_APP_STATE_INSTALLED);
+ g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3");
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+ 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), ==, AS_APP_STATE_AVAILABLE);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_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), ==, AS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+}
+
+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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) G_MAXUINT,
+ NULL);
+ 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);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "Bingo",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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), ==, AS_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_NOT_SUPPORTED);
+ g_assert_true (!ret);
+ g_clear_error (&error);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_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_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+}
+
+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)
+{
+ AsAppState state = gs_app_get_state (app);
+ g_debug ("state now %s", as_app_state_to_string (state));
+ if (state == AS_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_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_DESKTOP);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/*/desktop/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/*/runtime/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+
+ /* 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), ==, AS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_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), ==, AS_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/*/source/test/*");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+}
+
+/* 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_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ g_assert_cmpint (gs_app_get_state (app_src), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app_src), ==, "test");
+ g_assert_cmpstr (gs_app_get_unique_id (app_src), ==, "*/*/*/source/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_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_src,
+ 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_src), ==, AS_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_APP_KIND_DESKTOP);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/*/desktop/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/runtime/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_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), ==, AS_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/*/source/test/*");
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+}
+
+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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, "file:///wont/work");
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_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_APP_KIND_DESKTOP);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron");
+#if FLATPAK_CHECK_VERSION(1,1,2)
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/chiron-origin/desktop/org.test.Chiron/master"));
+#else
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master"));
+#endif
+ 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_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ NULL);
+ 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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_APP_STATE_AVAILABLE);
+ gs_flatpak_app_set_repo_url (app_source, testdir_repourl);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) 0,
+ NULL);
+ 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);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "runtime",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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/runtime/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_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_DEFAULT;
+ } 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;
+ }
+
+ /* 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_APP_KIND_DESKTOP);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE_LOCAL);
+ 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_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/flatpak/desktop/org.test.Chiron/master"));
+ g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE);
+ } else {
+#if FLATPAK_CHECK_VERSION(1,1,2)
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/chiron-origin/desktop/org.test.Chiron/master"));
+#else
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app),
+ "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master"));
+#endif
+ g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF);
+ 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/runtime/org.test.Runtime/master");
+ g_assert_cmpint (gs_app_get_state (runtime), ==, AS_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), ==, AS_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 (app), ==, NULL);
+
+ /* search for the application */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "chiron",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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), ==, AS_APP_STATE_INSTALLED);
+ if (is_bundle) {
+#if FLATPAK_CHECK_VERSION(1,1,2)
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2),
+ "user/flatpak/chiron-origin/desktop/org.test.Chiron/master"));
+#else
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2),
+ "user/flatpak/org.test.Chiron-origin/desktop/org.test.Chiron/master"));
+#endif
+ } else {
+ /* Note: the origin is now test-1 because that remote was created from the
+ * RuntimeRepo= setting
+ */
+ g_assert_true (as_utils_unique_id_equal (gs_app_get_unique_id (app2),
+ "user/flatpak/test-1/desktop/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_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ NULL);
+ 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (runtime_source, "flatpak");
+ gs_app_set_state (runtime_source, AS_APP_STATE_INSTALLED);
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", runtime_source,
+ NULL);
+ 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);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "chiron",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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;
+ GsApp *old_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(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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_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_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) G_MAXUINT,
+ 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);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "Bingo",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME,
+ NULL);
+ 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), ==, AS_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), ==, AS_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 (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_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) 0, /* force now */
+ NULL);
+ 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 are two entries */
+ 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 they are the same GObject */
+ app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/*/org.test.Chiron/*");
+ g_assert_true (app_tmp == app);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_UPDATABLE_LIVE);
+ g_assert_cmpstr (gs_app_get_update_details (app), ==, "Version 1.2.4:\nThis is best.\n\nVersion 1.2.3:\nThis is better.");
+ 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);
+
+ /* check that the runtime is not the update's one */
+ old_runtime = gs_app_get_runtime (app);
+ g_assert_true (old_runtime != NULL);
+ g_assert_cmpstr (gs_app_get_branch (old_runtime), !=, "new_master");
+
+ /* 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), ==, AS_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 (app), ==, NULL);
+ g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+ 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/runtime/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) == AS_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/runtime/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/runtime/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_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+}
+
+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;
+
+ /* drop all caches */
+ gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL);
+ gs_plugin_loader_setup_again (plugin_loader);
+
+ /* 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_APP_KIND_SOURCE);
+ gs_app_set_management_plugin (app_source, "flatpak");
+ gs_app_set_state (app_source, AS_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_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_INSTALLED);
+
+ /* refresh the appstream metadata */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) G_MAXUINT,
+ 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);
+
+ /* find available application */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", "Bingo",
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ 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), ==, AS_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), ==, AS_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/*/runtime/org.test.Chiron.Extension/master");
+ g_assert_nonnull (extension);
+ g_assert_cmpint (gs_app_get_state (extension), ==, AS_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_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) 0, /* force now */
+ NULL);
+ 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_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/*/org.test.Chiron/*");
+ g_assert_true (app_tmp == app);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_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 ();
+#if !FLATPAK_CHECK_VERSION(1,7,3)
+ /* Older flatpak versions don't have the API we use to propagate state
+ * between extension and app
+ */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+#else
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED);
+#endif
+ 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);
+
+ /* check the extension's state after the update */
+ g_assert_cmpint (gs_app_get_state (extension), ==, AS_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);
+
+ /* getting the runtime for later removal */
+ runtime = gs_app_get_runtime (app);
+
+ /* 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), ==, AS_APP_STATE_AVAILABLE);
+
+ /* remove the remote */
+ g_object_unref (plugin_job);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE,
+ "app", app_source,
+ 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_source), ==, AS_APP_STATE_AVAILABLE);
+
+ /* 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;
+ const gchar *allowlist[] = {
+ "appstream",
+ "flatpak",
+ "icons",
+ 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. */
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_content_type_set_mime_dirs (NULL);
+#endif
+
+ /* Similarly, add the system-wide icon theme path before it’s
+ * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */
+ gs_test_expose_icon_theme_paths ();
+
+ g_test_init (&argc, &argv,
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ G_TEST_OPTION_ISOLATE_DIRS,
+#endif
+ NULL);
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+ 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);
+
+ /* only critical and error are fatal */
+ g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+
+ /* we can only load this once per process */
+ plugin_loader = gs_plugin_loader_new ();
+ gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR);
+ gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE);
+ ret = gs_plugin_loader_setup (plugin_loader,
+ (gchar**) 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..0afc5a9
--- /dev/null
+++ b/plugins/flatpak/meson.build
@@ -0,0 +1,70 @@
+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-appstream.c',
+ '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,
+ link_with : [
+ libgnomesoftware
+ ]
+)
+metainfo = 'org.gnome.Software.Plugin.Flatpak.metainfo.xml'
+
+i18n.merge_file(
+ input: metainfo + '.in',
+ output: metainfo,
+ type: 'xml',
+ po_dir: join_paths(meson.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,
+ link_with : [
+ libgnomesoftware
+ ],
+ 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 120000
index 0000000..9f2eb6a
--- /dev/null
+++ b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore
@@ -0,0 +1 @@
+../../app-with-runtime/org.test.Chiron/.gitignore \ No newline at end of file
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..acc7e66
--- /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 remote 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