]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: Support changelogs & appstream metadata
authorAdrian Vovk <adrianvovk@gmail.com>
Wed, 19 Jul 2023 18:43:58 +0000 (14:43 -0400)
committerTom Coldrick <thomas.coldrick@codethink.co.uk>
Fri, 12 Jul 2024 13:38:09 +0000 (14:38 +0100)
Makes it possible to specify URLs to a changelog and an appstream
catalog XML in the sysupdate.d/*.conf files. This will be passed along
to the clients of systemd-sysupdated, which can then present this data.

docs/APPSTREAM_BUNDLE.md [new file with mode: 0644]
man/sysupdate.d.xml
src/sysupdate/sysupdate-transfer.c
src/sysupdate/sysupdate-transfer.h
src/sysupdate/sysupdate.c

diff --git a/docs/APPSTREAM_BUNDLE.md b/docs/APPSTREAM_BUNDLE.md
new file mode 100644 (file)
index 0000000..f953e16
--- /dev/null
@@ -0,0 +1,118 @@
+---
+title: Appstream Bundle
+category: Interfaces
+layout: default
+SPDX-License-Identifier: LGPL-2.1-or-later
+---
+
+# Appstream Bundle
+
+NOTE: This document is a work-in-progress.
+
+NOTE: This isn't yet implemented in libappstream and the software centers.
+
+[Appstream catalogs](https://www.freedesktop.org/software/appstream/docs/chap-CatalogData.html)
+are a standardized way to expose metadata about system components, apps, and updates to software
+centers (i.e. GNOME Software and KDE Discover). The `<bundle/>` tag links an appstream component
+to a packaging format. This is used by the software centers to decide which code path (or plugin)
+should handle the component. For instance: components with a `<bundle type="package">...</bundle>`
+will be handled by [PackageKit](https://www.freedesktop.org/software/PackageKit/), and components
+with a `<bundle type="flatpak">...</bundle>` will be handled by [libflatpak](https://docs.flatpak.org/).
+This document will define how to format an appstream component's `<bundle>` tag such that software
+centers will know to manage it using systemd. The following syntax will be supported:
+
+A `type="systemd"` attribute. This tells the software center that it should treat the bundle tag
+as described in this document.
+
+A `class=""` attribute, with the following possible values: `sysupdate`, `extension`, `confext`,
+or `portable`. These correspond to sysupdate components, sysexts, confexts, and portable services
+respectively.
+
+The value of the tag will be used as the name of the image (corresponding to the `class=` attribute).
+So for instance, `<bundle type="systemd" class="extension">foobar</bundle>` corresponds to a sysext
+named "foobar". For `class="sysupdate"`, there is a special case: if the value is empty, then the
+bundle actually refers to the host system.
+
+## Examples
+
+```xml
+<component type="addon">
+       <id>com.example.Devel</id>
+       <extends>com.example.OS</extends>
+       <name>Development Tools</name>
+       <summary>Tools essential to develop Example OS</summary>
+       <provides>
+               <binary>gcc</binary>
+               <binary>g++</binary>
+               <binary>make</binary>
+               <binary>autoconf</binary>
+               <binary>cmake</binary>
+               <binary>meson</binary>
+               <binary>ninja</binary>
+       </provides>
+       <developer_name>Example, inc.</developer_name>
+       <releases>
+               <release version="45" date="2024-01-15" />
+               <release version="44" date="2023-12-08" />
+               <release version="43" date="2023-11-10" />
+       </releases>
+       <bundle type="systemd" class="extension">devel</bundle>
+</component>
+```
+
+defines a sysext named `devel` to be presented by the software center. It will be
+updated via `systemd-sysupdated`'s `extension:devel` target. It will be treated
+as a plugin for the operating system itself.
+
+```xml
+<component merge="append">
+       <id>com.example.OS</id>
+       <releases>
+               <release version="45" date="2024-01-15" urgency="high">
+                       <description>
+                               <p>This release includes various bug fixes and performance improvements</p>
+                       </description>
+               </release>
+       </releases>
+       <bundle type="systemd" class="sysupdate" />
+</component>
+```
+
+extends existing appstream metadata for the host OS with a changelog. It also tells the software
+center that the host OS should be updated using the `host` target for `systemd-sysupdated`.
+
+```xml
+<component type="service">
+       <id>com.example.Foobar</id>
+       <name>Foobar Service</name>
+       <summary>Service that does foo to bar</summary>
+       <icon type="remote">https://example.com/products/foobar/logo.svg</icon>
+       <url type="homepage">https://example.com/products/foobar</url>
+       <provides>
+               <dbus type="system">com.example.Foobar</dbus>
+       </provides>
+       <developer_name>Example, inc.</developer_name>
+       <releases>
+               <release version="1.0.1" date="2024-02-16" urgency="critical">
+                       <description>
+                               <p>This release fixes a major security vulnerability. Please update ASAP.</p>
+                       </description>
+                       <issues>
+                               <issue type="cve">CVE-2024-28153</issue>
+                       </issues>
+               </release>
+               <release version="1.1-beta" date="2024-01-08" type="development" />
+               <release version="1.0" date="2023-11-23">
+                       <description>
+                               <p>Initial release!</p>
+                       </description>
+               </release>
+       </releases>
+       <bundle type="systemd" class="portable">foobar</bundle>
+</component>
+```
+
+defines a portable service named `foobar` to be presented by the software center. It will be
+updated via `systemd-sysupdated`'s `portable:foobar` target. It will be marked as an
+urgent update. It will be presented to the user with a display name, a description, and
+a custom icon.
index 125c5802b7e9769585d7a2905f26e8bfc0f00caa..ef3b21d29d4e2b386863b32f9d66704e52f2a1ee 100644 (file)
         <xi:include href="version-info.xml" xpointer="v251"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>ChangeLog=</varname></term>
+
+        <listitem><para>Optionally takes a human-presentable URL to a website containing a change-log of
+        the resource being updated.</para>
+
+        <para>This may be set multiple times in a single transfer definition. If set multiple times, the
+        values are gathered into a list of URLs. Adding a value of the empty string will clear the existing
+        list of all values.</para>
+
+        <para>This setting supports specifier expansion. See below for details on supported
+        specifiers. This setting will also expand the <literal>@v</literal> wildcard pattern. See above
+        for details.</para>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>AppStream=</varname></term>
+
+        <listitem><para>Optionally takes a URL to an
+        <ulink url="https://www.freedesktop.org/software/appstream/docs/chap-CatalogData.html">AppStream catalog</ulink>
+        XML file. This may be used by software centers (such as GNOME Software or KDE Discover) to present
+        rich metadata about the resources being updated. This includes display names, changelogs, icons,
+        and more. The specified catalog must include <ulink url="https://systemd.io/APPSTREAM_BUNDLE">special metadata</ulink>
+        to be correctly associated with <command>systemd-sysupdate</command> by the software centers.</para>
+
+        <para>This setting supports specifier expansion. See below for details on supported
+        specifiers.</para>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 3756798193852d71bd12232faed58b4611d0b10c..b1b3c662f643266ac0fad0088ad2134c65918220 100644 (file)
@@ -50,6 +50,9 @@ Transfer *transfer_free(Transfer *t) {
         free(t->current_symlink);
         free(t->final_path);
 
+        strv_free(t->changelog);
+        strv_free(t->appstream);
+
         partition_info_destroy(&t->partition_info);
 
         resource_destroy(&t->source);
@@ -168,6 +171,48 @@ static int config_parse_min_version(
         return free_and_replace(*version, resolved);
 }
 
+static int config_parse_url_specifiers(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+        char ***s = ASSERT_PTR(data);
+        _cleanup_free_ char *resolved = NULL;
+        int r;
+
+        assert(rvalue);
+
+        if (isempty(rvalue)) {
+                *s = strv_free(*s);
+                return 0;
+        }
+
+        r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
+                return 0;
+        }
+
+        if (!http_url_is_valid(resolved)) {
+                log_syntax(unit, LOG_WARNING, filename, line, 0,
+                           "%s= URL is not valid, ignoring: %s", lvalue, rvalue);
+                return 0;
+        }
+
+        r = strv_push(s, TAKE_PTR(resolved));
+        if (r < 0)
+                return log_oom();
+
+        return 0;
+}
+
 static int config_parse_current_symlink(
                 const char *unit,
                 const char *filename,
@@ -431,6 +476,8 @@ int transfer_read_definition(Transfer *t, const char *path) {
                 { "Transfer",    "MinVersion",              config_parse_min_version,          0, &t->min_version             },
                 { "Transfer",    "ProtectVersion",          config_parse_protect_version,      0, &t->protected_versions      },
                 { "Transfer",    "Verify",                  config_parse_bool,                 0, &t->verify                  },
+                { "Transfer",    "ChangeLog",               config_parse_url_specifiers,       0, &t->changelog               },
+                { "Transfer",    "AppStream",               config_parse_url_specifiers,       0, &t->appstream               },
                 { "Source",      "Type",                    config_parse_resource_type,        0, &t->source.type             },
                 { "Source",      "Path",                    config_parse_resource_path,        0, &t->source                  },
                 { "Source",      "PathRelativeTo",          config_parse_resource_path_relto,  0, &t->source.path_relative_to },
index 41a849430ac7861055e96619b10c21ef6d046c93..c28b68cc9221babff2d127333ef9dfc1482e7233 100644 (file)
@@ -26,6 +26,9 @@ struct Transfer {
         uint64_t instances_max;
         bool remove_temporary;
 
+        char **changelog;
+        char **appstream;
+
         /* When creating a new partition/file, optionally override these attributes explicitly */
         sd_id128_t partition_uuid;
         bool partition_uuid_set;
index f3af1a306a6fb21dc1fb2a76430426495080e8e4..000f6fa767bbd9b2f648a3f4a7a5daa5baac5d1e 100644 (file)
@@ -162,8 +162,10 @@ static int context_read_definitions(
                                        "No transfer definitions found.");
         }
 
-        for (size_t i = 0; i < c->n_transfers; i++) {
-                r = transfer_resolve_paths(c->transfers[i], root, node);
+        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
+                Transfer *t = *tr;
+
+                r = transfer_resolve_paths(t, root, node);
                 if (r < 0)
                         return r;
         }
@@ -480,6 +482,7 @@ static int context_show_version(Context *c, const char *version) {
                 have_read_only = false, have_growfs = false, have_sha256 = false;
         _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
         _cleanup_(table_unrefp) Table *t = NULL;
+        _cleanup_strv_free_ char **changelog_urls = NULL;
         UpdateSet *us;
         int r;
 
@@ -521,13 +524,30 @@ static int context_show_version(Context *c, const char *version) {
         table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
 
         /* Determine if the target will make use of partition/fs attributes for any of the transfers */
-        for (size_t n = 0; n < c->n_transfers; n++) {
-                Transfer *tr = c->transfers[n];
+        FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
+                Transfer *tr = *transfer;
 
                 if (tr->target.type == RESOURCE_PARTITION)
                         show_partition_columns = true;
                 if (RESOURCE_IS_FILESYSTEM(tr->target.type))
                         show_fs_columns = true;
+
+                STRV_FOREACH(changelog, tr->changelog) {
+                        assert(*changelog);
+
+                        _cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
+                        if (!changelog_url)
+                                return log_oom();
+
+                        /* Avoid duplicates */
+                        if (strv_contains(changelog_urls, changelog_url))
+                                continue;
+
+                        /* changelog_urls takes ownership of expanded changelog_url */
+                        r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
+                        if (r < 0)
+                                return log_oom();
+                }
         }
 
         for (size_t n = 0; n < us->n_instances; n++) {
@@ -666,13 +686,14 @@ static int context_show_version(Context *c, const char *version) {
         if (!have_sha256)
                 (void) table_hide_column_from_display(t, 12);
 
+
         if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
                 printf("%s%s%s Version: %s\n"
                        "    State: %s%s%s\n"
                        "Installed: %s%s\n"
                        "Available: %s%s\n"
                        "Protected: %s%s%s\n"
-                       " Obsolete: %s%s%s\n\n",
+                       " Obsolete: %s%s%s\n",
                        strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
                        strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
                        yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
@@ -680,6 +701,15 @@ static int context_show_version(Context *c, const char *version) {
                        FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
                        us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
 
+                STRV_FOREACH(url, changelog_urls) {
+                        _cleanup_free_ char *changelog_link = NULL;
+                        r = terminal_urlify(*url, NULL, &changelog_link);
+                        if (r < 0)
+                                return log_oom();
+                        printf("ChangeLog: %s\n", changelog_link);
+                }
+                printf("\n");
+
                 return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
         } else {
                 _cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
@@ -694,6 +724,7 @@ static int context_show_version(Context *c, const char *version) {
                                           SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
                                           SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
                                           SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
+                                          SD_JSON_BUILD_PAIR_STRV("changelog_urls", changelog_urls),
                                           SD_JSON_BUILD_PAIR_VARIANT("contents", t_json));
                 if (r < 0)
                         return log_error_errno(r, "Failed to create JSON: %m");
@@ -990,6 +1021,7 @@ static int verb_list(int argc, char **argv, void *userdata) {
         _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
         _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
         _cleanup_(context_freep) Context* context = NULL;
+        _cleanup_strv_free_ char **appstream_urls = NULL;
         const char *version;
         int r;
 
@@ -1013,8 +1045,8 @@ static int verb_list(int argc, char **argv, void *userdata) {
                 _cleanup_strv_free_ char **versions = NULL;
                 const char *current = NULL;
 
-                for (size_t i = 0; i < context->n_update_sets; i++) {
-                        UpdateSet *us = context->update_sets[i];
+                FOREACH_ARRAY(update_set, context->update_sets, context->n_update_sets) {
+                        UpdateSet *us = *update_set;
 
                         if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
                             FLAGS_SET(us->flags, UPDATE_NEWEST))
@@ -1025,8 +1057,20 @@ static int verb_list(int argc, char **argv, void *userdata) {
                                 return log_oom();
                 }
 
+                FOREACH_ARRAY(tr, context->transfers, context->n_transfers)
+                        STRV_FOREACH(appstream_url, (*tr)->appstream) {
+                                /* Avoid duplicates */
+                                if (strv_contains(appstream_urls, *appstream_url))
+                                        continue;
+
+                                r = strv_extend(&appstream_urls, *appstream_url);
+                                if (r < 0)
+                                        return log_oom();
+                        }
+
                 r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("current", current),
-                                          SD_JSON_BUILD_PAIR_STRV("all", versions));
+                                          SD_JSON_BUILD_PAIR_STRV("all", versions),
+                                          SD_JSON_BUILD_PAIR_STRV("appstream_urls", appstream_urls));
                 if (r < 0)
                         return log_error_errno(r, "Failed to create JSON: %m");