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