LVFS Technical White Paper

I spent a good chunk of today writing a technical whitepaper titled Introducing the Linux Vendor Firmware Service — I’d really appreciate any comments, either from people who have seen all progress from the start or who don’t know anything about it at all.

Typos, or more general comments are all welcome and once I’ve got something a bit more polished I’ll be sending this to some important suits in a few well known companies. Thanks for any help!

External Plugins in GNOME Software (6)

This is my last post about the gnome-software plugin structure. If you want more, join the mailing list and ask a question. If you’re not sure how something works then I’ve done a poor job on the docs, and I’m happy to explain as much as required.

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 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:

 
   /* 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);
+  }

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 add_installed() 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 migrating the UI widgets as the object changes and things like progress bars just magically work.

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 gs_plugin_cache_invalidate() 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.

And one last thing: If you’re thinking of building an out-of-tree plugin for production use ask yourself if it actually belongs upstream. Upstream plugins get ported as the API evolves, and I’m already happily carrying Ubuntu and Fedora-specific plugins that either self-disable at runtime or are protected using --enable-foo configure argument.

External Plugins in GNOME Software (5)

This is my penultimate post about the gnome-software plugin structure. If you’ve followed everything so far, well done.

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 appstream plugin. To do this you just save an AppStream-format XML file in either /usr/share/app-info/xmls/, /var/cache/app-info/xmls/ or ~/.local/share/app-info/xmls/. GNOME Software will immediately notice any new files, or changes to existing files as it has set up the various inotify watches.

This allows plugins to care a lot less about how applications are going to be shown. For example, the steam plugin downloads and parses the descriptions from a remote service during gs_plugin_refresh(), 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 gs_app_new("steam:foo.desktop"). All the search tokenisation and matching is done automatically, so it makes the plugin much simpler and faster.

The only extra step the steam plugin needs to do is implement the gs_plugin_adopt_app() function. This is called when an application does not have a management plugin set, and allows the plugin to claim the application for itself so it can handle installation, removal and updating. In the case of steam it could check the ID has a prefix of steam: or could check some other plugin-specific metadata using gs_app_get_metadata_item().

Another good example is the fwupd 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:

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");
}

The next (and last!) blog post I’m going to write is about the per-plugin cache that’s available to plugins to help speed up some operations. In related news, we now have a mailing list, so if you’re interested in this stuff I’d encourage you to join and ask questions there. I also released gnome-software 3.21.2 this morning, so if you want to try all this plugin stuff yourself your distro if probably going to be updating packages soon.

External Plugins in GNOME Software (4)

After my last post, I wanted to talk more about the refine functionality in gnome-software. 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.

Lets looks at another example.

gboolean
gs_plugin_refine_app (GsPlugin *plugin,
                      GsApp *app,
                      GsPluginRefineFlags flags,
                      GCancellable *cancellable,
                      GError **error)
{
  /* not required */
  if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0)
    return TRUE;

  /* 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;
}

This is a simple example, but shows what a plugin needs to do. It first checks if the action is required, in this case GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE. 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 gs_app_list_add(). Think of refine as adding things that cost time to calculate only when really required.

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 gnome-software --verbose on the command line will tell you why an application isn’t shown along with any extra data.

As a last point, a few people have worries that these blogs are perhaps asking for trouble; external plugins have a chequered history in a number of projects and I’m sure gnome-software would be in an even worse position given that the core maintainer team is still so small. Being honest, if we break your external plugin due to an API change in the core you probably should have pushed your changes upstream sooner. There’s a reason you have to build with -DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE

External Plugins in GNOME Software (3)

Lots of nice feedback from my last post, so here’s some new stuff. Up now is downloading new metadata and updates in plugins.

The plugin loader supports a gs_plugin_refresh() 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 infinite. This basically means the plugin must just ensure that any data exists no matter what the age.

Usually once per day, we’ll call gs_plugin_refresh() 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 gs_utils_get_file_age() utility helper can help you work out the cache age of file, or the plugin can handle it some other way.

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 commandline 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 gs_plugin_refresh() method is called again, but this time with GS_PLUGIN_REFRESH_FLAGS_PAYLOAD set. This is where the Flatpak plugin would download any ostree trees (but not doing the deloy 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 GS_PLUGIN_REFRESH_FLAGS_METADATA and then downloads the large firmware files themselves only when the GS_PLUGIN_REFRESH_FLAGS_PAYLOAD flag is set.

If the @app parameter is set for gs_plugin_download_file() 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 NULL which means something is happening globally which the UI can handle how it wants, for instance showing a loading page at startup.

gboolean
gs_plugin_refresh (GsPlugin *plugin,
                   guint cache_age,
                   GsPluginRefreshFlags flags,
                   GCancellable *cancellable,
                   GError **error)
{
  const gchar *metadata_fn = "/var/cache/example/metadata.xml";
  const gchar *metadata_url = "http://www.example.com/new.xml";

  /* this is called at startup and once per day */
  if (flags & GS_PLUGIN_REFRESH_FLAGS_METADATA) {
    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");
    }
  }

  /* this is called when the session is idle */
  if ((flags & GS_PLUGIN_REFRESH_FLAGS_PAYLOAD) == 0) {
    // FIXME: download any required updates now
  }

  return TRUE;
}

Note, if the downloading fails it’s okay to return FALSE; the plugin loader continues to run all plugins and just logs an error to the console. We’ll be calling into gs_plugin_refresh() again in only a few hours, so there’s no need to bother the user. For actions like gs_plugin_app_install 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.

External Plugins in GNOME Software (2)

After quite a lot of positive feedback from my last post I’ll write some more about custom plugins. Next up is returning custom applications into 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. So, code:

#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 ("example: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 */
  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_ui (app, "The example plugin");
  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 (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_set_icon (app, icon);

  /* return new app */
  gs_app_list_add (list, app);

  return TRUE;
}

This shows a lot of the plugin architecture in action. Some notable points:

  • The application ID (example:chiron.desktop) has a prefix of example which means we can co-exist with any package or flatpak version of the Chiron application, not setting the prefix would make the UI confused if more than one chiron.desktop got added.
  • Setting the management plugin means we can check for this string when working out if we can handle the install or remove action.
  • Most applications want a kind of AS_APP_KIND_DESKTOP to be visible as an application.
  • The origin is where the application originated from — usually this will be something like Fedora Updates.
  • The GS_APP_KUDO_INSTALLS_USER_DOCS means we get the blue “Documentation” award in the details page; there are many kudos to award to deserving apps.
  • Setting the license means we don’t get the non-free warning — removing the 3rd party warning can be done using AS_APP_QUIRK_PROVENANCE
  • The icons plugin will take the stock icon and convert it to a pixbuf of the correct size.

To show this fake application just compile and install the plugin, touch ~/chiron and then restart gnome-software.

Screenshot from 2016-05-20 21-22-38

By filling in the optional details (which can also be filled in using gs_plugin_refine_app() (to be covered in a future blog post) 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.

Screenshot from 2016-05-20 21-22-46

For anyone interested, I’m also slowly writing up these blog posts into proper docbook and uploading them with the gtk-doc files here. I think this documentation would have been really useful for the Endless and Ubuntu people a few weeks ago, so if anyone sees any typos or missing details please let me know.

External plugins in GNOME Software

I’ve just pushed a set of patches to gnome-software master that allow people to compile out-of-tree gnome-software plugins.

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. However, now we’re shipping gnome-software in enterprise-class distros we might want to allow customers to ship thier 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 “application is a desktop-file” paradigm. This is where a plugin makes sense, and something I’d like to support in future updates to RHEL 7.

At this point it probably makes sense to talk a bit about how the architecture of gnome-software works. At its heart it’s just a big 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.

There are broadly 3 types of plugin methods:

  • Actions: Do something on a specific GsApp; install gimp.desktop
  • Refine: Get details about a specific GsApp; is firefox.desktop installed? or get reviews for inkscape.desktop
  • Adopt: Can this plugin handle this GsApp; can fwupd handle com.hughski.ColorHug2.firmware

You only need to define the vfuncs that the plugin needs, and the name is taken automatically from the suffix of the .so file. So, lets look at a sample plugin one chunk at a time, taking it nice and slow. First the copyright and licence (it only has to be GPLv2+ if it’s headed upstream):

/*
 * Copyright (C) 2016 Richard Hughes 
 * Licensed under the GNU General Public License Version 2
 */

Then, the magic header that sucks in everything that’s exported:

#include <gnome-software.h>

Then we have to define when our plugin is run in reference to other plugins, as we’re such a simple plugin we’re relying on another plugin to run after us to actually make the GsApp “complete”, i.e. adding icons and long descriptions:

void
gs_plugin_initialize (GsPlugin *plugin)
{
  gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream");
}

Then we can start to do something useful. In this example I want to show GIMP as a result (from any provider, e.g. flatpak or a distro package) when the user searches exactly for fotoshop. There is no prefixing or stemming being done for simplicity.

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, AS_APP_QUIRK_MATCH_ANY_PREFIX);
      gs_app_list_add (list, app);
    }
  }
  return TRUE;
}

We can then easily build and install the plugin using:

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`

Screenshot from 2016-05-19 10-39-53

I’m going to be cleaning up the exported API and adding some more developer documentation before I release the next tarball, but if this is useful to you please let me know and I’ll do some more blog posts explaining more how the internal architecture of gnome-software works, and how you can do different things with plugins.

3rd Party Fedora Repositories and AppStream

I was recently asked how to make 3rd party repositories add apps to GNOME Software. This is relevant if you run a internal private repo for employee tools, or are just kind enough to provide a 3rd party repo for Fedora or RHEL users for your free or non-free applications.

In most cases people are already running something like this to generate the repomd metadata files on a directory of RPM files:

createrepo_c --no-database --simple-md-filenames SRPMS/
createrepo_c --no-database --simple-md-filenames x86_64/

So, we need to actually generate the AppStream XML. This works by exploding any interesting .rpm files and merging together the .desktop file, the .appdata.xml file and preprocessing some icons. Only applications installing AppData files will be shown in GNOME Software, so you might need to fix before you start.

appstream-builder			\
	--origin=yourcompanyname	\
	--basename=appstream		\
	--cache-dir=/tmp/asb-cache	\
	--enable-hidpi			\
	--max-threads=1			\
	--min-icon-size=32		\
	--output-dir=/tmp/asb-md	\
	--packages-dir=x86_64/		\
	--temp-dir=/tmp/asb-icons

This takes a second or two (or 40 minutes if you’re trying to process the entire Fedora archive…) and spits out some files to /tmp/asb-md — you probably want to change some things there to make more sense for your build server.

We then have to take the generated XML and the tarball of icons and add it to the repomd.xml master document so that GNOME Software (via PackageKit) automatically downloads the content for searching. This is as simple as doing:

modifyrepo_c				\
	--no-compress			\
	--simple-md-filenames		\
	/tmp/asb-md/appstream.xml.gz	\
	x86_64/repodata/
modifyrepo_c				\
	--no-compress			\
	--simple-md-filenames		\
	/tmp/asb-md/appstream-icons.tar.gz	\
	x86_64/repodata/

Any questions, please ask. If you’re using a COPR then all these steps are done for you automatically. If you’re using xdg-app already, then this is all magically done for you as well, and automatically downloaded by GNOME Software.

Upgrading Fedora 23 to 24 using GNOME Software

I’ve spent the last couple of days fixing up all the upgrade bugs in GNOME Software and backporting them to gnome-3-20. The idea is that we backport gnome-software plus a couple of the deps into Fedora 23 so that we can offer a 100% GUI upgrade experience. It’s the first time we’ve officially transplanted a n+1 GNOME component into an older release (ignoring my unofficial Fedora 20 whole-desktop backport COPR) and so we’re carefully testing for regressions and new bugs.

If you do want to test upgrading from F23 to F24, first make sure you’ve backed up your system. Then, install and enable this COPR and update gnome-software. This should also install a new libhif, libappstream-glib, json-glib and PackageKit and a few other bits. If you’ve not done the update offline using [the old] GNOME Software, you’ll need to reboot at this stage as well.

Fire up the new gnome-software and look at the new UI. Actually, there’s not a lot new to see as we’ve left new features like the ODRS reviewing service and xdg-app as F24-only features, so it should be mostly the same as before but with better search results. Now go to the Updates page which will show any updates you have pending, and it will also download the list of possible distro upgrades to your home directory.

As we’re testing upgrading to a pre-release, we have to convince gnome-software that we’re living in the future. First, open ~/.cache/gnome-software/3.20/upgrades/fedora.json and search for f24. Carefully change the Under Development string to Active then save the file. Log out, back in and the launch gnome-software again or wait for the notification from the shell. If all has gone well you should see a banner telling you about the new upgrade. If you click Download go and get a coffee and start baking a cake, as it’s going to take a long time to download all that new goodness. Once complete just click Install, which prompts a reboot where the packages will be installed. For this step you’ll probably want to bake another cake. We’re not quite in an atomic instant-apply world yet, although I’ll be talking a lot more about that for Fedora 25.

With a bit of luck, after 30 minutes staring at a progressbar the computer should reboot itself into a fresh new Fedora 24 beta installation. Success!

Screenshot_Fedora23-Upgrade_2016-04-20_15:23:27

If you spot any problems or encounter any bugs, please let me know either in bugzilla, email or or IRC. I’ve not backported all the custom CSS for the upgrade banner just yet, but this should be working soon. Thanks!

Age Ratings in GNOME Software: Introducing OARS?

In GNOME Software we show lots of applications ranging from games aimed at pre-schoolers to applications explicitly designed to download, well, porn. A concept that is fairly well understood by parents is age ratings, and there are well known and trusted ratings bodies such as the ESRB and PEGI, as well as other country-specific schemes. Parents can use the ratings to control what kind of content is available to install, and vendors can use the ratings as a legal (or common-sense) control who gets to purchase what.

The ratings systems between countries are varied, varying from descriptions such as “M” which will be familiar for US users, “R” for Australian users to the slightly more obvious “18+” rating for European users. The differing ratings authorities define what is allowed in each category in slightly different way, some allowing mild profanity for a “7+” rating, and others none at all. Some countries consider drug taking in a video game to be no more dangerous as to mild cursing, other countries consider this on the same level as sexual violence.

OARS

So, we’re sunk, right? Nearly. There exists a group called “International Age Rating Coalition” which allows developers to register (sometimes for free), answer a simple questionnaire and out pops the ratings they should use for various countries. The IARC is made up of the regulatory bodies all over the planet, and so you can use the actual trademarked age rating images for your product. ish.

If you want to build a a software center, say GNOME Software for example, you have to pay a license fee. A $100,000 annual fee, plus extra per application shown in the software center. This is prohibitive for us, and would mean we couldn’t have the same functionality in other software center interfaces.

We could easily provide in the AppData files details about the application/game. This can be combined with a rule engine specific to the country of viewing, which would pop out a rating. I think the ESRB would be hard pushed to trademark “M” as an age rating, although I completely agree they have correctly and sensibly trademarked the stylized logo for the PG rating, along with the “ESRB” name itself. I don’t think this should stop us using an “PG” or “M” rating in the software center as long as we avoid these trademarks and copyrights.

I’m happy to work on a new system to both generate the AppData upstream information from a questionaire, and the rule engines that processes these rules and pops out a rating. The question then becomes, is this useful? Is this something that people would actually want? Comments welcome.

p.s. OARS: “Open Age Rating System”, name is work in process.