diff --git a/docs/APPSTREAM_BUNDLE.md b/docs/APPSTREAM_BUNDLE.md
new file mode 100644
index 0000000000..f953e169d0
--- /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 125c5802b7..ef3b21d29d 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 3756798193..b1b3c662f6 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 41a849430a..c28b68cc92 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 f3af1a306a..000f6fa767 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");