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.
This commit is contained in:
Adrian Vovk 2023-07-19 14:43:58 -04:00 committed by Tom Coldrick
parent 42c0b689a8
commit db8849f2d4
No known key found for this signature in database
GPG Key ID: 504B7DF0B0C123BF
5 changed files with 252 additions and 8 deletions

118
docs/APPSTREAM_BUNDLE.md Normal file
View File

@ -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.

View File

@ -487,6 +487,38 @@
<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>

View 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 },

View 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;

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