diff options
Diffstat (limited to 'doc/api')
-rw-r--r-- | doc/api/gnome-software-docs.xml | 617 | ||||
-rw-r--r-- | doc/api/gnome-software.types | 6 | ||||
-rw-r--r-- | doc/api/gs-example-details.png | bin | 0 -> 53714 bytes | |||
-rw-r--r-- | doc/api/gs-example-installed.png | bin | 0 -> 106482 bytes | |||
-rw-r--r-- | doc/api/gs-example-search.png | bin | 0 -> 24934 bytes | |||
-rw-r--r-- | doc/api/meson.build | 11 |
6 files changed, 634 insertions, 0 deletions
diff --git a/doc/api/gnome-software-docs.xml b/doc/api/gnome-software-docs.xml new file mode 100644 index 0000000..fb43ff2 --- /dev/null +++ b/doc/api/gnome-software-docs.xml @@ -0,0 +1,617 @@ +<?xml version="1.0"?> +<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN" + "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" +[ + <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'"> +]> +<book id="index"> + <bookinfo> + <title>GNOME Software Reference Manual</title> + </bookinfo> + + <reference id="tutorial"> + <title>GNOME Software Plugin Tutorial</title> + <partintro> + <para> + GNOME Software is a software installer designed to be easy to use. + </para> + + <section> + <title>Introduction</title> + <para> + At the heart of gnome software the application is just a plugin loader + that has some GTK UI that gets created for various result types. + The idea is we have lots of small plugins that each do one thing and + then pass the result onto the other plugins. + These are ordered by dependencies against each other at runtime and + each one can do things like editing an existing application or adding a + new application to the result set. + This is how we can add support for things like firmware updating, + Steam, GNOME Shell web-apps and flatpak bundles without making big + changes all over the source tree. + </para> + <para> + There are broadly 3 types of plugin methods: + </para> + <itemizedlist> + <listitem> + <para><emphasis role="strong">Actions</emphasis>: Do something on a specific GsApp</para> + </listitem> + <listitem> + <para><emphasis role="strong">Refine</emphasis>: Get details about a specific GsApp</para> + </listitem> + <listitem> + <para><emphasis role="strong">Adopt</emphasis>: Can this plugin handle this GsApp</para> + </listitem> + </itemizedlist> + <para> + In general, building things out-of-tree isn't something that I think is + a very good idea; the API and ABI inside gnome-software is still + changing and there's a huge benefit to getting plugins upstream where + they can undergo review and be ported as the API adapts. + I'm also super keen to provide configurability in GSettings for doing + obviously-useful things, the sort of thing Fleet Commander can set for + groups of users. + </para> + <para> + However, now we're shipping gnome-software in enterprise-class distros + we might want to allow customers to ship their own plugins to make + various business-specific changes that don't make sense upstream. + This might involve querying a custom LDAP server and changing the + suggested apps to reflect what groups the user is in, or might involve + showing a whole new class of applications that does not conform to the + Linux-specific <emphasis>application is a desktop-file</emphasis> paradigm. + This is where a plugin makes sense. + </para> + + <para> + The plugin only needs to define the vfuncs that are required, and the + plugin name is taken automatically from the suffix of the + <filename>.so</filename> file. + </para> + <example> + <title>A sample plugin</title> + <programlisting> +/* + * Copyright (C) 2016 Richard Hughes + */ + +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream"); +} + +gboolean +gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + guint i; + for (i = 0; values[i] != NULL; i++) { + if (g_strcmp0 (values[i], "fotoshop") == 0) { + g_autoptr(GsApp) app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + } + return TRUE; +} + </programlisting> + </example> + + <para> + We have to define when our plugin is run in reference to other plugins, + in this case, making sure we run before <code>appstream</code>. + As we're such a simple plugin we're relying on another plugin to run + after us to actually make the GsApp <emphasis>complete</emphasis>, + i.e. loading icons and setting a localised long description. + </para> + <para> + In this example we want to show GIMP as a result (from any provider, + e.g. flatpak or a distro package) when the user searches exactly for + <code>fotoshop</code>. + </para> + <para> + We can then build and install the plugin using: + </para> + <informalexample> + <programlisting> +gcc -shared -o libgs_plugin_example.so gs-plugin-example.c -fPIC \ + `pkg-config --libs --cflags gnome-software` \ + -DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE && + sudo cp libgs_plugin_example.so `pkg-config gnome-software --variable=plugindir` + </programlisting> + </informalexample> + + <mediaobject id="gs-example-search"> + <imageobject> + <imagedata format="PNG" fileref="gs-example-search.png" align="center"/> + </imageobject> + </mediaobject> + + </section> + + <section> + <title>Distribution Specific Functionality</title> + <para> + Some plugins should only run on specific distributions, for instance + the <code>fedora-pkgdb-collections</code> plugin should only be used on + Fedora systems. + This can be achieved with a simple runtime check using the helper + <code>gs_plugin_check_distro_id()</code> method or the <code>GsOsRelease</code> + object where more complicated rules are required. + </para> + <example> + <title>Self disabling on other distributions</title> + <programlisting> +void +gs_plugin_initialize (GsPlugin *plugin) +{ + if (!gs_plugin_check_distro_id (plugin, "ubuntu")) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + /* allocate private data etc. */ +} + </programlisting> + </example> + + </section> + + <section> + <title>Custom Applications in the Installed List</title> + <para> + Next is returning custom applications in the installed list. + The use case here is a proprietary software distribution method that + installs custom files into your home directory, but you can use your + imagination for how this could be useful. + The example here is all hardcoded, and a true plugin would have to + derive the details about the GsApp, for example reading in an XML + file or YAML config file somewhere. + </para> + <example> + <title>Example showing a custom installed application</title> + <programlisting> +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); +} + +gboolean +gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *fn = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(AsIcon) icon = NULL; + + /* check if the app exists */ + fn = g_build_filename (g_get_home_dir (), "chiron", NULL); + if (!g_file_test (fn, G_FILE_TEST_EXISTS)) + return TRUE; + + /* the trigger exists, so create a fake app */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_management_plugin (app, "example"); + gs_app_set_kind (app, AS_APP_KIND_DESKTOP); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application"); + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, + "Chiron is the name of an application.\n\n" + "It can be used to demo some of our features"); + + /* these are all optional, but make details page looks better */ + gs_app_set_version (app, "1.2.3"); + gs_app_set_size_installed (app, 2 * 1024 * 1024); + gs_app_set_size_download (app, 3 * 1024 * 1024); + gs_app_set_origin_hostname (app, "http://www.teaching-example.org/"); + gs_app_add_category (app, "Game"); + gs_app_add_category (app, "ActionGame"); + gs_app_add_kudo (app, GS_APP_KUDO_INSTALLS_USER_DOCS); + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, "GPL-2.0+ and LGPL-2.1+"); + + /* create a stock icon which will be loaded by the 'icons' plugin */ + icon = as_icon_new (); + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + as_icon_set_name (icon, "input-gaming"); + gs_app_add_icon (app, icon); + + /* return new app */ + gs_app_list_add (list, app); + + return TRUE; +} + </programlisting> + </example> + <para> + This shows a lot of the plugin architecture in action. Some notable points: + </para> + <itemizedlist> + <listitem> + <para> + Setting the management plugin means we can check for this string + when working out if we can handle the install or remove action. + </para> + </listitem> + <listitem> + <para> + Most applications want a kind of <code>AS_APP_KIND_DESKTOP</code> + to be visible as an application. + </para> + </listitem> + <listitem> + <para> + The origin is where the application originated from -- usually + this will be something like <emphasis>Fedora Updates</emphasis>. + </para> + </listitem> + <listitem> + <para> + The <code>GS_APP_KUDO_INSTALLS_USER_DOCS</code> means we get the + blue "Documentation" award in the details page; there are many + kudos to award to deserving apps. + </para> + </listitem> + <listitem> + <para> + Setting the license means we don't get the non-free warning -- + removing the 3rd party warning can be done using + <code>GS_APP_QUIRK_PROVENANCE</code> + </para> + </listitem> + <listitem> + <para> + The <code>icons</code> plugin will take the stock icon and convert + it to a pixbuf of the correct size. + </para> + </listitem> + </itemizedlist> + <para> + To show this fake application just compile and install the plugin, + <code>touch ~/chiron</code> and then restart gnome-software. + To avoid restarting <filename>gnome-software</filename> each time a + proper plugin would create a <code>GFileMonitor</code> object to + monitor files. + </para> + + <mediaobject id="gs-example-installed"> + <imageobject> + <imagedata format="PNG" fileref="gs-example-installed.png" align="center"/> + </imageobject> + </mediaobject> + + <para> + By filling in the optional details (which can also be filled in using + <code>gs_plugin_refine()</code> you can also make the details + page a much more exciting place. + Adding a set of screenshots is left as an exercise to the reader. + </para> + + <mediaobject id="gs-example-details"> + <imageobject> + <imagedata format="PNG" fileref="gs-example-details.png" align="center"/> + </imageobject> + </mediaobject> + + </section> + + <section> + <title>Downloading Metadata and Updates</title> + + <para> + The plugin loader supports a <code>gs_plugin_refresh()</code> vfunc that + is called in various situations. + To ensure plugins have the minimum required metadata on disk it is + called at startup, but with a cache age of <emphasis>infinite</emphasis>. + This basically means the plugin must just ensure that + <emphasis role="strong">any</emphasis> data exists no matter what the age. + </para> + <para> + Usually once per hour, we'll call <code>gs_plugin_refresh()</code> but + with the correct cache age set (typically a little over 24 hours) which + allows the plugin to download new metadata or payload files from remote + servers. + The <code>gs_utils_get_file_age()</code> utility helper can help you + work out the cache age of file, or the plugin can handle it some other + way. + </para> + <para> + For the Flatpak plugin we just make sure the AppStream metadata exists + at startup, which allows us to show search results in the UI. + If the metadata did not exist (e.g. if the user had added a remote + using the command-line without gnome-software running) then we would + show a loading screen with a progress bar before showing the main UI. + On fast connections we should only show that for a couple of seconds, + but it's a good idea to try any avoid that if at all possible in the + plugin. + Once per day the <code>gs_plugin_get_updates()</code> method is called, + and then <code>gs_plugin_download_app()</code> may be called if the + user has configured automatic updates. + This is where the Flatpak plugin would download any ostree trees (but + not doing the deploy step) so that the applications can be updated live + in the details panel without having to wait for the download to complete. + In a similar way, the fwupd plugin downloads the tiny LVFS metadata with + <code>gs_plugin_refresh()</code> and then downloads the large firmware + files themselves when <code>gs_plugin_download_app()</code> or + <code>gs_plugin_download_app()</code> is called. + </para> + <para> + If the <code>@app</code> parameter is set for + <code>gs_plugin_download_file()</code> then the progress of the download + is automatically proxied to the UI elements associated with the + application, for instance the install button would show a progress bar + in the various different places in the UI. + For a refresh there's no relevant GsApp to use, so we'll leave it + <code>NULL</code> which means something is happening globally which the + UI can handle how it wants, for instance showing a loading page at startup. + </para> + <para> + Note, if the downloading fails it's okay to return <code>FALSE</code>; + the plugin loader continues to run all plugins and just logs an error + to the console. We'll be calling into <code>gs_plugin_refresh()</code> + again in only another hour, so there's no need to bother the user. + For actions like <code>gs_plugin_app_install</code> we also do the same + thing, but we also save the error on the GsApp itself so that the UI is + free to handle that how it wants, for instance showing a GtkDialog + window for example. + </para> + <example> + <title>Refresh example</title> + <programlisting> +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + const gchar *metadata_fn = "/var/cache/example/metadata.xml"; + const gchar *metadata_url = "http://www.example.com/new.xml"; + g_autoptr(GFile) file = g_file_new_for_path (metadata_fn); + + /* is the metadata missing or too old */ + if (gs_utils_get_file_age (file) > cache_age) { + if (!gs_plugin_download_file (plugin, + NULL, + metadata_url, + metadata_fn, + cancellable, + error)) { + /* it's okay to fail here */ + return FALSE; + } + g_debug ("successfully downloaded new metadata"); + } + return TRUE; +} + </programlisting> + </example> + </section> + + <section> + <title>Adding Application Information Using Refine</title> + + <para> + As previous examples have shown it's very easy to add a new + application to the search results, updates list or installed list. + Some plugins don't want to add more applications, but want to modify + existing applications to add more information depending on what is + required by the UI code. + The reason we don't just add everything at once is that for + search-as-you-type to work effectively we need to return results in + less than about 50ms and querying some data can take a long time. + For example, it might take a few hundred ms to work out the download + size for an application when a plugin has to also look at what + dependencies are already installed. + We only need this information once the user has clicked the search + results and when the user is in the details panel, so we can save a + ton of time not working out properties that are not useful. + </para> + <example> + <title>Refine example</title> + <programlisting> +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* already set */ + if (gs_app_get_license (app) != NULL) + return TRUE; + + /* FIXME, not just hardcoded! */ + if (g_strcmp0 (gs_app_get_id (app, "chiron.desktop") == 0)) + gs_app_set_license (app, "GPL-2.0 and LGPL-2.0+"); + } + + return TRUE; +} + </programlisting> + </example> + <para> + This is a simple example, but shows what a plugin needs to do. + It first checks if the action is required, in this case + <code>GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE</code>. + This request is more common than you might expect as even the search + results shows a non-free label if the license is unspecified or + non-free. + It then checks if the license is already set, returning with success + if so. + If not, it checks the application ID and hardcodes a license; in the + real world this would be querying a database or parsing an additional + config file. + As mentioned before, if the license value is freely available without + any extra work then it's best just to set this at the same time as + when adding the app with <code>gs_app_list_add()</code>. + Think of refine as <emphasis>adding things that cost time to + calculate only when really required</emphasis>. + </para> + <para> + The UI in gnome-software is quite forgiving for missing data, hiding + sections or labels as required. + Some things are required however, and forgetting to assign an icon or + short description will get the application vetoed so that it's not + displayed at all. + Helpfully, running <code>gnome-software --verbose</code> on the + command line will tell you why an application isn't shown along with + any extra data. + </para> + + </section> + + <section> + <title>Adopting AppStream Applications</title> + + <para> + There's a lot of flexibility in the gnome-software plugin structure; + a plugin can add custom applications and handle things like search and + icon loading in a totally custom way. + Most of the time you don't care about how search is implemented or how + icons are going to be loaded, and you can re-use a lot of the existing + code in the <code>appstream</code> plugin. + To do this you just save an AppStream-format XML file in either + <filename>/usr/share/app-info/xmls/</filename>, + <filename>/var/cache/app-info/xmls/</filename> or + <filename>~/.local/share/app-info/xmls/</filename>. + GNOME Software will immediately notice any new files, or changes to + existing files as it has set up the various inotify watches. + </para> + <para> + This allows plugins to care a lot less about how applications are + going to be shown. + For example, the <code>steam</code> plugin downloads and parses the + descriptions from a remote service during + <code>gs_plugin_refresh()</code>, and also finds the best icon types + and downloads them too. + Then it exports the data to an AppStream XML file, saving it to your + home directory. + This allows all the applications to be easily created (and then refined) + using something as simple as <code>gs_app_new("steam:foo.desktop")</code>. + All the search tokenisation and matching is done automatically, + so it makes the plugin much simpler and faster. + </para> + <para> + The only extra step the steam plugin needs to do is implement the + <code>gs_plugin_adopt_app()</code> function. + This is called when an application does not have a management plugin + set, and allows the plugin to <emphasis>claim</emphasis> the + application for itself so it can handle installation, removal and + updating. + </para> + <para> + Another good example is the <code>fwupd</code> that wants to handle + any firmware we've discovered in the AppStream XML. + This might be shipped by the vendor in a package using Satellite, + or downloaded from the LVFS. It wouldn't be kind to set a management + plugin explicitly in case XFCE or KDE want to handle this in a + different way. This adoption function in this case is trivial: + </para> + + <informalexample> + <programlisting> +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_kind (app) == AS_APP_KIND_FIRMWARE) + gs_app_set_management_plugin (app, "fwupd"); +} + </programlisting> + </informalexample> + </section> + + <section> + <title>Using The Plugin Cache</title> + + <para> + GNOME Software used to provide a per-process plugin cache, + automatically de-duplicating applications and trying to be smarter + than the plugins themselves. + This involved merging applications created by different plugins and + really didn't work very well. + For versions 3.20 and later we moved to a per-plugin cache which + allows the plugin to control getting and adding applications to the + cache and invalidating it when it made sense. + This seems to work a lot better and is an order of magnitude less + complicated. + Plugins can trivially be ported to using the cache using something + like this: + </para> + <informalexample> + <programlisting> + /* create new object */ + id = gs_plugin_flatpak_build_id (inst, xref); +- app = gs_app_new (id); ++ app = gs_plugin_cache_lookup (plugin, id); ++ if (app == NULL) { ++ app = gs_app_new (id); ++ gs_plugin_cache_add (plugin, id, app); ++ } + </programlisting> + </informalexample> + <para> + Using the cache has two main benefits for plugins. + The first is that we avoid creating duplicate GsApp objects for the + same logical thing. + This means we can query the installed list, start installing an + application, then query it again before the install has finished. + The GsApp returned from the second <code>add_installed()</code> + request will be the same GObject, and thus all the signals connecting + up to the UI will still be correct. + This means we don't have to care about <emphasis>migrating</emphasis> + the UI widgets as the object changes and things like progress bars just + magically work. + </para> + <para> + The other benefit is more obvious. + If we know the application state from a previous request we don't have + to query a daemon or do another blocking library call to get it. + This does of course imply that the plugin is properly invalidating + the cache using <code>gs_plugin_cache_invalidate()</code> which it + should do whenever a change is detected. + Whether a plugin uses the cache for this reason is up to the plugin, + but if it does it is up to the plugin to make sure the cache doesn't + get out of sync. + </para> + </section> + + </partintro> + </reference> + + <reference id="api"> + <partintro> + <para> + This documentation is auto-generated. + If you see any issues, please file bugs. + </para> + </partintro> + <title>GNOME Software Plugin API</title> + <xi:include href="xml/gs-app-list.xml"/> + <xi:include href="xml/gs-app.xml"/> + <xi:include href="xml/gs-category.xml"/> + <xi:include href="xml/gs-os-release.xml"/> + <xi:include href="xml/gs-plugin.xml"/> + <xi:include href="xml/gs-plugin-event.xml"/> + <xi:include href="xml/gs-plugin-vfuncs.xml"/> + <xi:include href="xml/gs-utils.xml"/> + </reference> + +</book> diff --git a/doc/api/gnome-software.types b/doc/api/gnome-software.types new file mode 100644 index 0000000..5f0a7b0 --- /dev/null +++ b/doc/api/gnome-software.types @@ -0,0 +1,6 @@ +gs_app_get_type +gs_app_list_get_type +gs_category_get_type +gs_os_release_get_type +gs_plugin_event_get_type +gs_plugin_get_type diff --git a/doc/api/gs-example-details.png b/doc/api/gs-example-details.png Binary files differnew file mode 100644 index 0000000..88c29e4 --- /dev/null +++ b/doc/api/gs-example-details.png diff --git a/doc/api/gs-example-installed.png b/doc/api/gs-example-installed.png Binary files differnew file mode 100644 index 0000000..0cd424a --- /dev/null +++ b/doc/api/gs-example-installed.png diff --git a/doc/api/gs-example-search.png b/doc/api/gs-example-search.png Binary files differnew file mode 100644 index 0000000..77611bd --- /dev/null +++ b/doc/api/gs-example-search.png diff --git a/doc/api/meson.build b/doc/api/meson.build new file mode 100644 index 0000000..aa8f873 --- /dev/null +++ b/doc/api/meson.build @@ -0,0 +1,11 @@ +gnome.gtkdoc( + 'gnome-software', + src_dir : join_paths(meson.source_root(), 'lib'), + main_xml : 'gnome-software-docs.xml', + html_assets : [ + 'gs-example-details.png', + 'gs-example-installed.png', + 'gs-example-search.png' + ], + install : true +) |