Skip to content

Commit

Permalink
Add a search CLI verb and DBus API
Browse files Browse the repository at this point in the history
This closes a longstanding feature request and improves compatibility
with the `dnf`/`yum` CLI.

The feature set and output text intentionally matches that tool, e.g.
globs like `rpm-ostree search kernel*` and multi term searches like
`rpm-ostree search kernel python` are supported.

Search results per section are limited to 50 due to DBus message size
limits.

Closes: #1877
  • Loading branch information
lukewarmtemp committed Jun 21, 2023
1 parent 7ad41b6 commit 37bb4bc
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 1 deletion.
14 changes: 14 additions & 0 deletions man/rpm-ostree.xml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,20 @@ Boston, MA 02111-1307, USA.
</listitem>
</varlistentry>

<varlistentry>
<term><command>search</command></term>

<listitem>
<para>
Takes one or more query terms as arguments. The packages are
searched within the enabled repositories in
<filename>/etc/yum.repos.d/</filename>. Packages can be
overlayed and removed using the <command>install</command>
and <command>uninstall</command> commands.
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><command>rebase</command></term>

Expand Down
2 changes: 2 additions & 0 deletions src/app/libmain.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ static RpmOstreeCommand commands[] = {
"Overlay additional packages", rpmostree_builtin_install },
{ "uninstall", static_cast<RpmOstreeBuiltinFlags> (RPM_OSTREE_BUILTIN_FLAG_CONTAINER_CAPABLE),
"Remove overlayed additional packages", rpmostree_builtin_uninstall },
{ "search", static_cast<RpmOstreeBuiltinFlags> (RPM_OSTREE_BUILTIN_FLAG_CONTAINER_CAPABLE),
"Search for packages", rpmostree_builtin_search },
{ "override", static_cast<RpmOstreeBuiltinFlags> (RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD),
"Manage base package overrides", rpmostree_builtin_override },
{ "reset", static_cast<RpmOstreeBuiltinFlags> (RPM_OSTREE_BUILTIN_FLAG_SUPPORTS_PKG_INSTALLS),
Expand Down
1 change: 1 addition & 0 deletions src/app/rpmostree-builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ BUILTINPROTO (internals);
BUILTINPROTO (container);
BUILTINPROTO (install);
BUILTINPROTO (uninstall);
BUILTINPROTO (search);
BUILTINPROTO (override);
BUILTINPROTO (kargs);
BUILTINPROTO (reset);
Expand Down
101 changes: 101 additions & 0 deletions src/app/rpmostree-pkg-builtins.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

#include <gio/gunixfdlist.h>

#include <set>

static char *opt_osname;
static gboolean opt_reboot;
static gboolean opt_dry_run;
Expand Down Expand Up @@ -310,3 +312,102 @@ rpmostree_builtin_uninstall (int argc, char **argv, RpmOstreeCommandInvocation *
return pkg_change (invocation, sysroot_proxy, FALSE, (const char *const *)opt_install,
(const char *const *)argv, cancellable, error);
}

struct cstrless
{
bool
operator() (const gchar *a, const gchar *b) const
{
return strcmp (a, b) < 0;
}
};

gboolean
rpmostree_builtin_search (int argc, char **argv, RpmOstreeCommandInvocation *invocation,
GCancellable *cancellable, GError **error)
{
GOptionContext *context;
glnx_unref_object RPMOSTreeSysroot *sysroot_proxy = NULL;

context = g_option_context_new ("PACKAGE [PACKAGE...]");
g_option_context_add_main_entries (context, install_option_entry, NULL);
g_option_context_add_main_entries (context, uninstall_option_entry, NULL);

if (!rpmostree_option_context_parse (context, option_entries, &argc, &argv, invocation,
cancellable, NULL, NULL, &sysroot_proxy, error))
return FALSE;

if (argc < 2)
{
rpmostree_usage_error (context, "At least one PACKAGE must be specified", error);
return FALSE;
}

glnx_unref_object RPMOSTreeOS *os_proxy = NULL;

if (!rpmostree_load_os_proxy (sysroot_proxy, opt_osname, cancellable, &os_proxy, error))
return FALSE;

g_autoptr (GPtrArray) arg_names = g_ptr_array_new ();
for (guint i = 1; i < argc; i++)
{
g_ptr_array_add (arg_names, (char *)argv[i]);
}
g_ptr_array_add (arg_names, NULL);

g_autoptr(GVariant) out_packages = NULL;

if (!rpmostree_os_call_search_sync (os_proxy, (const char *const *)arg_names->pdata,
&out_packages, cancellable, error))
return FALSE;

g_autoptr (GVariantIter) iter1 = NULL;
g_variant_get (out_packages, "aa{sv}", &iter1);

g_autoptr (GVariantIter) iter2 = NULL;
std::set<const gchar *, cstrless> query_set;

while (g_variant_iter_loop (iter1, "a{sv}", &iter2))
{
const gchar *key;
const gchar *name;
const gchar *summary;
const gchar *query;
const gchar *match_group = "";

g_autoptr (GVariant) value = NULL;

while (g_variant_iter_loop (iter2, "{sv}", &key, &value))
{
if (strcmp (key, "key") == 0)
g_variant_get (value, "s", &query);
else if (strcmp (key, "name") == 0)
g_variant_get (value, "s", &name);
else if (strcmp (key, "summary") == 0)
g_variant_get (value, "s", &summary);
}

if (!query_set.count (query))
{
query_set.insert (query);

if (strcmp (query, "match_group_a") == 0)
match_group = "Summary & Name";
else if (strcmp (query, "match_group_b") == 0)
match_group = "Name";
else if (strcmp (query, "match_group_c") == 0)
match_group = "Summary";

g_print ("\n===== %s Matched =====\n", match_group);
}

g_print ("%s : %s\n", name, summary);
}

if (query_set.size () == 0)
{
g_print ("No matches found.");
}

return TRUE;
}
6 changes: 6 additions & 0 deletions src/daemon/org.projectatomic.rpmostree1.xml
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,12 @@
<arg type="aa{sv}" name="packages" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
</method>

<method name="Search">
<arg type="as" name="names" direction="in"/>
<arg type="aa{sv}" name="packages" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
</method>
</interface>

<interface name="org.projectatomic.rpmostree1.OSExperimental">
Expand Down
112 changes: 111 additions & 1 deletion src/daemon/rpmostreed-os.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
#include "rpmostreed-transaction.h"
#include "rpmostreed-utils.h"

#include <libdnf/hy-util.cpp>

#include <set>
#include <vector>

typedef struct _RpmostreedOSClass RpmostreedOSClass;

struct _RpmostreedOS
Expand Down Expand Up @@ -141,7 +146,7 @@ os_authorize_method (GDBusInterfaceSkeleton *interface, GDBusMethodInvocation *i
else if (g_strcmp0 (method_name, "GetDeploymentBootConfig") == 0
|| g_strcmp0 (method_name, "ListRepos") == 0
|| g_strcmp0 (method_name, "WhatProvides") == 0
|| g_strcmp0 (method_name, "GetPackages") == 0)
|| g_strcmp0 (method_name, "GetPackages") == 0 || g_strcmp0 (method_name, "Search") == 0)
{
/* Note: early return here because no need authentication
* for these methods
Expand Down Expand Up @@ -1138,6 +1143,110 @@ os_handle_get_packages (RPMOSTreeOS *interface, GDBusMethodInvocation *invocatio
return TRUE;
}

/* helper function to sort and search within a set of (const gchar *) */
struct cstrless
{
bool
operator() (const gchar *a, const gchar *b) const
{
return strcmp (a, b) < 0;
}
};

/* wrapper function to both query for packages and add them to the builder */
static void
query_results_to_builder (HyQuery query, GVariantBuilder *builder, const gchar *id)
{
std::set<const gchar *, cstrless> result_set;
g_autoptr (GPtrArray) pkglist = hy_query_run (query);
for (guint i = 0; i < pkglist->len && result_set.size () < 50; i++)
{
auto pkg = static_cast<DnfPackage *> (g_ptr_array_index (pkglist, i));
if (!result_set.count (dnf_package_get_name (pkg)))
{
os_add_package_info_to_builder (pkg, builder, id);
result_set.insert (dnf_package_get_name (pkg));
}
}
}

/* helper function to filter package query results */
static void
search_packages_by_filter (HyQuery query, GVariantBuilder *builder, const gchar *const *names,
std::vector<int> keynames, const gchar *id)
{
/* case insensitive exact match between search term and package */
hy_query_clear (query);
for (guint j = 0; j < keynames.size (); j++)
{
for (guint i = 0; names[i] != NULL; i++)
{
if (!hy_is_glob_pattern (names[i]))
{
hy_query_filter (query, keynames[j], HY_EQ | HY_ICASE, names[i]);
}
else
{
hy_query_filter (query, keynames[j], HY_GLOB | HY_ICASE, names[i]);
}
}
}
query_results_to_builder (query, builder, id);

/* case insensitive substring match of search term within package */
hy_query_clear (query);
for (guint j = 0; j < keynames.size (); j++)
{
for (guint i = 0; names[i] != NULL; i++)
{
if (!hy_is_glob_pattern (names[i]))
{
hy_query_filter (query, keynames[j], HY_SUBSTR | HY_ICASE, names[i]);
}
else
{
hy_query_filter (query, keynames[j], HY_GLOB | HY_ICASE, names[i]);
}
}
}
query_results_to_builder (query, builder, id);
}

static gboolean
os_handle_search (RPMOSTreeOS *interface, GDBusMethodInvocation *invocation,
const gchar *const *names)
{
GError *local_error = NULL;
g_autoptr (GCancellable) cancellable = NULL;

sd_journal_print (LOG_INFO, "Handling Search for caller %s",
g_dbus_method_invocation_get_sender (invocation));

g_autoptr (DnfContext) dnfctx
= os_create_dnf_context_simple (interface, TRUE, cancellable, &local_error);
if (dnfctx == NULL)
return os_throw_dbus_invocation_error (invocation, &local_error);

hy_autoquery HyQuery query = hy_query_create (dnf_context_get_sack (dnfctx));

GVariantBuilder builder;
g_variant_builder_init (&builder, (const GVariantType *)"aa{sv}");

std::vector<int> keynames_a = { HY_PKG_NAME, HY_PKG_SUMMARY };
search_packages_by_filter (query, &builder, names, keynames_a, "match_group_a");

std::vector<int> keynames_b = { HY_PKG_NAME };
search_packages_by_filter (query, &builder, names, keynames_b, "match_group_b");

std::vector<int> keynames_c = { HY_PKG_SUMMARY };
search_packages_by_filter (query, &builder, names, keynames_c, "match_group_c");

GVariant *pkgs_result = g_variant_builder_end (&builder);
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(@aa{sv})", pkgs_result));

return TRUE;
}

/* This is an older variant of Cleanup, kept for backcompat */
static gboolean
os_handle_clear_rollback_target (RPMOSTreeOS *interface, GDBusMethodInvocation *invocation,
Expand Down Expand Up @@ -1813,6 +1922,7 @@ rpmostreed_os_iface_init (RPMOSTreeOSIface *iface)
iface->handle_finalize_deployment = os_handle_finalize_deployment;
iface->handle_what_provides = os_handle_what_provides;
iface->handle_get_packages = os_handle_get_packages;
iface->handle_search = os_handle_search;
/* legacy cleanup API; superseded by Cleanup() */
iface->handle_clear_rollback_target = os_handle_clear_rollback_target;
/* legacy deployment change API; superseded by UpdateDeployment() */
Expand Down

0 comments on commit 37bb4bc

Please sign in to comment.