From: Adrian Vovk Date: Wed, 19 Jul 2023 18:43:58 +0000 (-0400) Subject: sysupdate: Support changelogs & appstream metadata X-Git-Tag: v257-rc1~913^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=db8849f2d4ab24c767f806f324587939e7f5c8e6;p=thirdparty%2Fsystemd.git sysupdate: Support changelogs & appstream metadata 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. --- diff --git a/docs/APPSTREAM_BUNDLE.md b/docs/APPSTREAM_BUNDLE.md new file mode 100644 index 00000000000..f953e169d02 --- /dev/null +++ b/docs/APPSTREAM_BUNDLE.md @@ -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 `` 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 `...` +will be handled by [PackageKit](https://www.freedesktop.org/software/PackageKit/), and components +with a `...` will be handled by [libflatpak](https://docs.flatpak.org/). +This document will define how to format an appstream component's `` 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, `foobar` 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 + + com.example.Devel + com.example.OS + Development Tools + Tools essential to develop Example OS + + gcc + g++ + make + autoconf + cmake + meson + ninja + + Example, inc. + + + + + + devel + +``` + +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 + + com.example.OS + + + +

This release includes various bug fixes and performance improvements

+
+
+
+ +
+``` + +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 + + com.example.Foobar + Foobar Service + Service that does foo to bar + https://example.com/products/foobar/logo.svg + https://example.com/products/foobar + + com.example.Foobar + + Example, inc. + + + +

This release fixes a major security vulnerability. Please update ASAP.

+
+ + CVE-2024-28153 + +
+ + + +

Initial release!

+
+
+
+ foobar +
+``` + +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. diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml index 125c5802b7e..ef3b21d29d4 100644 --- a/man/sysupdate.d.xml +++ b/man/sysupdate.d.xml @@ -487,6 +487,38 @@ + + ChangeLog= + + Optionally takes a human-presentable URL to a website containing a change-log of + the resource being updated. + + 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. + + This setting supports specifier expansion. See below for details on supported + specifiers. This setting will also expand the @v wildcard pattern. See above + for details. + + + + + + AppStream= + + Optionally takes a URL to an + AppStream catalog + 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 special metadata + to be correctly associated with systemd-sysupdate by the software centers. + + This setting supports specifier expansion. See below for details on supported + specifiers. + + + diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 37567981938..b1b3c662f64 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -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 }, diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h index 41a849430ac..c28b68cc922 100644 --- a/src/sysupdate/sysupdate-transfer.h +++ b/src/sysupdate/sysupdate-transfer.h @@ -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; diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index f3af1a306a6..000f6fa767b 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -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");