diff --git a/app/Makefile.am.inc b/app/Makefile.am.inc index e3d811fd..034dab6c 100644 --- a/app/Makefile.am.inc +++ b/app/Makefile.am.inc @@ -37,8 +37,10 @@ flatpak_SOURCES = \ app/flatpak-builtins-document-unexport.c \ app/flatpak-builtins-document-info.c \ app/flatpak-builtins-document-list.c \ + app/flatpak-builtins-search.c \ $(NULL) -flatpak_LDADD = $(AM_LDADD) $(BASE_LIBS) $(OSTREE_LIBS) $(SOUP_LIBS) $(JSON_LIBS) libglnx.la libflatpak-common.la -flatpak_CFLAGS = $(AM_CFLAGS) $(BASE_CFLAGS) $(OSTREE_CFLAGS) $(SOUP_CFLAGS) $(JSON_CFLAGS) \ +flatpak_LDADD = $(AM_LDADD) $(BASE_LIBS) $(OSTREE_LIBS) $(SOUP_LIBS) $(JSON_LIBS) $(APPSTREAM_GLIB_LIBS) \ + libglnx.la libflatpak-common.la +flatpak_CFLAGS = $(AM_CFLAGS) $(BASE_CFLAGS) $(OSTREE_CFLAGS) $(SOUP_CFLAGS) $(JSON_CFLAGS) $(APPSTREAM_GLIB_CFLAGS) \ -DLOCALEDIR=\"$(localedir)\" diff --git a/app/flatpak-builtins-search.c b/app/flatpak-builtins-search.c new file mode 100644 index 00000000..4fd30893 --- /dev/null +++ b/app/flatpak-builtins-search.c @@ -0,0 +1,260 @@ +/* + * Copyright © 2017 Patrick Griffis + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Patrick Griffis + */ + +#include "config.h" + +#include +#include + +#include "flatpak-builtins.h" +#include "flatpak-dir.h" +#include "flatpak-table-printer.h" +#include "flatpak-utils.h" + +static gboolean opt_user; +static gboolean opt_system; + +static GOptionEntry options[] = { + { "user", 0, 0, G_OPTION_ARG_NONE, &opt_user, N_("Search only user installations"), NULL }, + { "system", 0, 0, G_OPTION_ARG_NONE, &opt_system, N_("Search only system-wide installations"), NULL }, + { NULL } +}; + +static GPtrArray * +get_remote_stores (GPtrArray *dirs, GCancellable *cancellable) +{ + GError *error = NULL; + GPtrArray *ret = g_ptr_array_new_with_free_func (g_object_unref); + for (guint i = 0; i < dirs->len; ++i) + { + FlatpakDir *dir = g_ptr_array_index (dirs, i); + g_autofree char *install_path = g_file_get_path (flatpak_dir_get_path (dir)); + g_auto(GStrv) remotes = flatpak_dir_list_enumerated_remotes (dir, cancellable, &error); + if (error) + { + g_warning ("%s", error->message); + g_clear_error (&error); + continue; + } + else if (remotes == NULL) + continue; + + for (guint j = 0; remotes[j]; ++j) + { + g_autofree char *appstream_path = g_build_filename (install_path, "appstream", remotes[j], + flatpak_get_arch(), "active", "appstream.xml.gz", + NULL); + g_autoptr(GFile) appstream_file = g_file_new_for_path (appstream_path); + g_autoptr(AsStore) store = as_store_new (); + // We want to see multiple versions/branches of same app-id's, e.g. org.gnome.Platform + as_store_set_add_flags (store, as_store_get_add_flags (store) | AS_STORE_ADD_FLAG_USE_UNIQUE_ID); + as_store_from_file (store, appstream_file, NULL, cancellable, &error); + if (error) + { + // We want to ignore this error as it is harmless and valid + // NOTE: appstream-glib doesn't have granular file-not-found error + if (!g_str_has_suffix (error->message, "No such file or directory")) + g_warning ("%s", error->message); + g_clear_error (&error); + continue; + } + + g_object_set_data_full (G_OBJECT(store), "remote-name", g_strdup(remotes[j]), g_free); + g_ptr_array_add (ret, g_steal_pointer (&store)); + } + } + return ret; +} + +typedef struct MatchResult { + AsApp *app; + GPtrArray *remotes; + guint score; +} MatchResult; + +static void +match_result_free (MatchResult *result) +{ + g_object_unref (result->app); + g_ptr_array_unref (result->remotes); + g_free (result); +} + +static MatchResult * +match_result_new (AsApp *app, guint score) +{ + MatchResult *result = g_new (MatchResult, 1); + result->app = g_object_ref (app); + result->remotes = g_ptr_array_new_with_free_func (g_free); + result->score = score; + return result; +} + +static void +match_result_add_remote (MatchResult *self, const char *remote) +{ + for (guint i = 0; i < self->remotes->len; ++i) + { + const char *remote_entry = g_ptr_array_index (self->remotes, i); + if (!strcmp (remote, remote_entry)) + return; + } + g_ptr_array_add (self->remotes, g_strdup(remote)); +} + +static int +compare_by_score (MatchResult *a, MatchResult *b, gpointer user_data) +{ + // Reverse order, higher score comes first + return (int)b->score - (int)a->score; +} + +static int +compare_apps (MatchResult *a, AsApp *b) +{ + return !as_app_equal (a->app, b); +} + +static const char * +get_comment_localized (AsApp *app) +{ + const char * const * languages = g_get_language_names (); + for (gsize i = 0; languages[i]; ++i) + { + const char *comment = as_app_get_comment (app, languages[i]); + if (comment != NULL) + return comment; + } + return NULL; +} + +static void +print_app (MatchResult *res, FlatpakTablePrinter *printer) +{ + AsRelease *release = as_app_get_release_default (res->app); + const char *version = release ? as_release_get_version (release) : NULL; + const char *id = as_app_get_id_filename (res->app); + const char *branch = as_app_get_branch (res->app); + + flatpak_table_printer_add_column (printer, id); + flatpak_table_printer_add_column (printer, version); + flatpak_table_printer_add_column (printer, branch); + flatpak_table_printer_add_column (printer, g_ptr_array_index (res->remotes, 0)); + for (guint i = 1; i < res->remotes->len; ++i) + flatpak_table_printer_append_with_comma (printer, g_ptr_array_index (res->remotes, i)); + flatpak_table_printer_add_column (printer, get_comment_localized (res->app)); + flatpak_table_printer_finish_row (printer); +} + +gboolean +flatpak_builtin_search (int argc, char **argv, GCancellable *cancellable, GError **error) +{ + g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GOptionContext) context = g_option_context_new (_("TEXT - Search remote apps/runtimes for text")); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + + if (!flatpak_option_context_parse (context, options, &argc, &argv, FLATPAK_BUILTIN_FLAG_NO_DIR, + NULL, cancellable, error)) + return FALSE; + + // Default: All system and user remotes + if (!opt_user && !opt_system) + opt_user = opt_system = TRUE; + + if (opt_user) + g_ptr_array_add (dirs, flatpak_dir_get_user ()); + + if (opt_system) + g_ptr_array_add (dirs, flatpak_dir_get_system_default ()); + + if (argc < 2) + return usage_error (context, _("TEXT must be specified"), error); + + const char *search_text = argv[1]; + GSList *matches = NULL; + + // We want a store for each remote so we keep the remote information + // as AsApp doesn't currently contain that information + g_autoptr(GPtrArray) remote_stores = get_remote_stores (dirs, cancellable); + for (guint j = 0; j < remote_stores->len; ++j) + { + AsStore *store = g_ptr_array_index (remote_stores, j); + GPtrArray *apps = as_store_get_apps (store); + for (guint i = 0; i < apps->len; ++i) + { + AsApp *app = g_ptr_array_index (apps, i); + const guint score = as_app_search_matches (app, search_text); + if (score == 0) + continue; + + // Avoid duplicate entries, but show multiple remotes + GSList *list_entry = g_slist_find_custom (matches, app, + (GCompareFunc)compare_apps); + MatchResult *result = NULL; + if (list_entry != NULL) + result = list_entry->data; + else + { + result = match_result_new (app, score); + matches = g_slist_insert_sorted_with_data (matches, result, + (GCompareDataFunc)compare_by_score, NULL); + } + match_result_add_remote (result, + g_object_get_data (G_OBJECT(store), "remote-name")); + } + } + + if (matches != NULL) + { + FlatpakTablePrinter *printer = flatpak_table_printer_new (); + int col = 0; + + flatpak_table_printer_set_column_title (printer, col++, _("Application ID")); + flatpak_table_printer_set_column_title (printer, col++, _("Version")); + flatpak_table_printer_set_column_title (printer, col++, _("Branch")); + flatpak_table_printer_set_column_title (printer, col++, _("Remotes")); + flatpak_table_printer_set_column_title (printer, col++, _("Description")); + g_slist_foreach (matches, (GFunc)print_app, printer); + flatpak_table_printer_print (printer); + flatpak_table_printer_free (printer); + + g_slist_free_full (matches, (GDestroyNotify)match_result_free); + } + else + { + g_print ("%s\n", _("No matches found")); + } + return TRUE; +} + +gboolean +flatpak_complete_search (FlatpakCompletion *completion) +{ + g_autoptr(GOptionContext) context = NULL; + + context = g_option_context_new (""); + if (!flatpak_option_context_parse (context, options, &completion->argc, &completion->argv, + FLATPAK_BUILTIN_FLAG_NO_DIR, NULL, NULL, NULL)) + return FALSE; + + flatpak_complete_options (completion, options); + flatpak_complete_options (completion, global_entries); + return TRUE; +} diff --git a/app/flatpak-builtins.h b/app/flatpak-builtins.h index 2970140b..700f2eda 100644 --- a/app/flatpak-builtins.h +++ b/app/flatpak-builtins.h @@ -85,6 +85,7 @@ BUILTINPROTO (document_list) BUILTINPROTO (override) BUILTINPROTO (repo) BUILTINPROTO (config) +BUILTINPROTO (search) #undef BUILTINPROTO diff --git a/app/flatpak-main.c b/app/flatpak-main.c index a4756932..9a76286d 100644 --- a/app/flatpak-main.c +++ b/app/flatpak-main.c @@ -66,6 +66,10 @@ static FlatpakCommand commands[] = { { "info", N_("Show info for installed app or runtime"), flatpak_builtin_info, flatpak_complete_info }, { "config", N_("Configure flatpak"), flatpak_builtin_config, flatpak_complete_config }, + /* translators: please keep the leading newline and space */ + { N_("\n Finding applications and runtimes") }, + { "search", N_("Search for remote apps/runtimes"), flatpak_builtin_search, flatpak_complete_search }, + /* translators: please keep the leading newline and space */ { N_("\n Running applications") }, { "run", N_("Run an application"), flatpak_builtin_run, flatpak_complete_run }, diff --git a/ci/build.sh b/ci/build.sh index c18e8e5b..2c0781dc 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -8,7 +8,7 @@ dn=$(dirname $0) pkg_install sudo which attr fuse \ libubsan libasan libtsan \ - elfutils git gettext-devel \ + elfutils git gettext-devel libappstream-glib-devel \ /usr/bin/{update-mime-database,update-desktop-database,gtk-update-icon-cache} pkg_install_testing ostree-devel ostree pkg_install_if_os fedora gjs parallel clang diff --git a/configure.ac b/configure.ac index 23a873ee..dfe3e46b 100644 --- a/configure.ac +++ b/configure.ac @@ -226,6 +226,8 @@ PKG_CHECK_MODULES(FUSE, [fuse]) PKG_CHECK_MODULES(JSON, [json-glib-1.0]) +PKG_CHECK_MODULES(APPSTREAM_GLIB, [appstream-glib]) + AC_ARG_ENABLE([seccomp], AC_HELP_STRING([--disable-seccomp], [Disable seccomp]), diff --git a/po/POTFILES.in b/po/POTFILES.in index 05336acc..cd005e6e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -26,6 +26,7 @@ app/flatpak-builtins-override.c app/flatpak-builtins-repo.c app/flatpak-builtins-repo-update.c app/flatpak-builtins-run.c +app/flatpak-builtins-search.c app/flatpak-builtins-uninstall.c app/flatpak-builtins-update.c app/flatpak-main.c