]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: add a "suggests" concept to features and components
authorLennart Poettering <lennart@amutable.com>
Wed, 24 Jun 2026 15:09:19 +0000 (17:09 +0200)
committerLennart Poettering <lennart@amutable.com>
Mon, 13 Jul 2026 15:36:10 +0000 (17:36 +0200)
Let's make it possible to "suggest" that certain features or components
are enabled under some conditions.

For this, both features and components gain two things:

1. A Suggested= field which takes a boolean. If true the
   feature/component will be suggested for installation, if false it
   will not.

2. A set of SuggestedOnXYZ= settings are modelled after ConditionXYZ= in
   unit files (and implement a subset of them), will suggest some
   component/feature under specific conditions.

The result of the condition is shown in the various output tools.

man/sysupdate.features.xml
src/shared/shared-forward.h
src/sysupdate/sysupdate-config.c
src/sysupdate/sysupdate-config.h
src/sysupdate/sysupdate-feature.c
src/sysupdate/sysupdate-feature.h
src/sysupdate/sysupdate.c
src/sysupdate/sysupdate.h

index 606292e2694ad8c6b3688b9809a3f7e305843cc0..d3a7a63810167e6b2e0e7879a0492597261f66cb 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>Suggest=</varname></term>
+
+        <listitem><para>Takes a boolean argument. If true, this feature is suggested for enablement, if
+        false it is not. This does not enable the feature on its own, but is a hint surfaced by the update
+        tools (for example via the <option>--feature-suggested</option> switch of
+        <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
+        so that the system administrator or higher-level tooling may act on it.</para>
+
+        <para>If this setting is not specified, the <varname>SuggestOn…=</varname> conditions described below
+        are evaluated instead to determine whether the feature is suggested. If neither this setting nor any
+        <varname>SuggestOn…=</varname> condition is specified, the feature is not suggested.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>SuggestOnArchitecture=</varname></term>
+        <term><varname>SuggestOnFirmware=</varname></term>
+        <term><varname>SuggestOnVirtualization=</varname></term>
+        <term><varname>SuggestOnHost=</varname></term>
+        <term><varname>SuggestOnFraction=</varname></term>
+        <term><varname>SuggestOnKernelCommandLine=</varname></term>
+        <term><varname>SuggestOnVersion=</varname></term>
+        <term><varname>SuggestOnCredential=</varname></term>
+        <term><varname>SuggestOnSecurity=</varname></term>
+        <term><varname>SuggestOnOSRelease=</varname></term>
+        <term><varname>SuggestOnMachineTag=</varname></term>
+
+        <listitem><para>Suggest this feature for enablement depending on system properties. These settings
+        take the same arguments and implement the same semantics — including the leading <literal>!</literal>
+        for negation — as the identically-named
+        <varname>ConditionArchitecture=</varname>, <varname>ConditionFirmware=</varname>,
+        <varname>ConditionVirtualization=</varname>, <varname>ConditionHost=</varname>,
+        <varname>ConditionFraction=</varname>, <varname>ConditionKernelCommandLine=</varname>,
+        <varname>ConditionVersion=</varname>, <varname>ConditionCredential=</varname>,
+        <varname>ConditionSecurity=</varname>, <varname>ConditionOSRelease=</varname> and
+        <varname>ConditionMachineTag=</varname> settings for unit files, which are documented in
+        <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+        The feature is suggested if all specified conditions apply. As with the <varname>Condition…=</varname>
+        settings, assigning an empty string to one of these resets the list.</para>
+
+        <para>These conditions are only evaluated if <varname>Suggest=</varname> is not specified.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 751a6f71dc359bab7f8a4ba87b913dd3dc882ef9..a9c7101e558981350026dd8901955ba620aa6b28 100644 (file)
@@ -19,6 +19,7 @@ typedef enum BusPrintPropertyFlags BusPrintPropertyFlags;
 typedef enum BusTransport BusTransport;
 typedef enum CatFlags CatFlags;
 typedef enum CertificateSourceType CertificateSourceType;
+typedef enum ConditionType ConditionType;
 typedef enum DnsAnswerFlags DnsAnswerFlags;
 typedef enum DnsCacheMode DnsCacheMode;
 typedef enum DnsOverTlsMode DnsOverTlsMode;
index 7f553a95f0b411734fa2f420fb3308cc2dd22e06..45794b5a212d05da09740eb39c8819461263b254 100644 (file)
@@ -1,8 +1,10 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include "alloc-util.h"
+#include "condition.h"
 #include "log.h"
 #include "specifier.h"
+#include "string-table.h"
 #include "string-util.h"
 #include "strv.h"
 #include "sysupdate-config.h"
@@ -48,6 +50,22 @@ int config_parse_url_specifiers(
         return free_and_replace(*s, resolved);
 }
 
+static const char* const suggest_on_type_table[_CONDITION_TYPE_MAX] = {
+        [CONDITION_ARCHITECTURE]        = "SuggestOnArchitecture",
+        [CONDITION_FIRMWARE]            = "SuggestOnFirmware",
+        [CONDITION_VIRTUALIZATION]      = "SuggestOnVirtualization",
+        [CONDITION_HOST]                = "SuggestOnHost",
+        [CONDITION_FRACTION]            = "SuggestOnFraction",
+        [CONDITION_KERNEL_COMMAND_LINE] = "SuggestOnKernelCommandLine",
+        [CONDITION_VERSION]             = "SuggestOnVersion",
+        [CONDITION_CREDENTIAL]          = "SuggestOnCredential",
+        [CONDITION_SECURITY]            = "SuggestOnSecurity",
+        [CONDITION_OS_RELEASE]          = "SuggestOnOSRelease",
+        [CONDITION_MACHINE_TAG]         = "SuggestOnMachineTag",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(suggest_on_type, ConditionType);
+
 int config_parse_url_specifiers_many(
                 const char *unit,
                 const char *filename,
@@ -90,3 +108,58 @@ int config_parse_url_specifiers_many(
 
         return 0;
 }
+
+int config_parse_condition(
+                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) {
+
+        const char *root = userdata;
+        ConditionType cond = ltype;
+        Condition **list = data, *c;
+        int r;
+
+        assert(filename);
+        assert(lvalue);
+        assert(rvalue);
+        assert(data);
+
+        if (isempty(rvalue)) {
+                *list = condition_free_list_type(*list, cond);
+                return 0;
+        }
+
+        bool trigger = rvalue[0] == '|';
+        if (trigger) {
+                rvalue++;
+                rvalue += strspn(rvalue, WHITESPACE);
+        }
+
+        bool negate = rvalue[0] == '!';
+        if (negate) {
+                rvalue++;
+                rvalue += strspn(rvalue, WHITESPACE);
+        }
+
+        _cleanup_free_ char *resolved = NULL;
+        r = specifier_printf(rvalue, SIZE_MAX, system_and_tmp_specifier_table, root, /* userdata= */ 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;
+        }
+
+        c = condition_new(cond, resolved, trigger, negate);
+        if (!c)
+                return log_oom();
+
+        LIST_PREPEND(conditions, *list, c);
+        return 0;
+}
index bd972d598d56ad7961234a0e88b6ef9193791e08..271430c73d5d870961452c649531b2ab117b8c2a 100644 (file)
@@ -2,6 +2,10 @@
 #pragma once
 
 #include "conf-parser-forward.h"
+#include "shared-forward.h"
 
 CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers);
 CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers_many);
+CONFIG_PARSER_PROTOTYPE(config_parse_condition);
+
+DECLARE_STRING_TABLE_LOOKUP_TO_STRING(suggest_on_type, ConditionType);
index 49c2e77988b93ba8b6a74fdf3ea029862efcafee..ef284f7f70177075a10aeba691e382c445f9bdde 100644 (file)
@@ -1,6 +1,9 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <unistd.h>
+
 #include "alloc-util.h"
+#include "condition.h"
 #include "conf-parser.h"
 #include "hash-funcs.h"
 #include "path-util.h"
@@ -18,6 +21,8 @@ static Feature *feature_free(Feature *f) {
         free(f->documentation);
         free(f->appstream);
 
+        condition_free_list(f->suggest_on);
+
         return mfree(f);
 }
 
@@ -30,6 +35,7 @@ Feature *feature_new(void) {
 
         *f = (Feature) {
                 .n_ref = 1,
+                .suggest = -1,
         };
 
         return f;
@@ -45,10 +51,22 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons
         assert(f);
 
         ConfigTableItem table[] = {
-                { "Feature", "Description",   config_parse_string,         0, &f->description },
-                { "Feature", "Documentation", config_parse_url_specifiers, 0, &f->documentation },
-                { "Feature", "AppStream",     config_parse_url_specifiers, 0, &f->appstream },
-                { "Feature", "Enabled",       config_parse_bool,           0, &f->enabled },
+                { "Feature", "Description",                config_parse_string,         0,                             &f->description   },
+                { "Feature", "Documentation",              config_parse_url_specifiers, 0,                             &f->documentation },
+                { "Feature", "AppStream",                  config_parse_url_specifiers, 0,                             &f->appstream     },
+                { "Feature", "Enabled",                    config_parse_bool,           0,                             &f->enabled       },
+                { "Feature", "Suggest",                    config_parse_tristate,       0,                             &f->suggest       },
+                { "Feature", "SuggestOnArchitecture",      config_parse_condition,      CONDITION_ARCHITECTURE,        &f->suggest_on    },
+                { "Feature", "SuggestOnFirmware",          config_parse_condition,      CONDITION_FIRMWARE,            &f->suggest_on    },
+                { "Feature", "SuggestOnVirtualization",    config_parse_condition,      CONDITION_VIRTUALIZATION,      &f->suggest_on    },
+                { "Feature", "SuggestOnHost",              config_parse_condition,      CONDITION_HOST,                &f->suggest_on    },
+                { "Feature", "SuggestOnFraction",          config_parse_condition,      CONDITION_FRACTION,            &f->suggest_on    },
+                { "Feature", "SuggestOnKernelCommandLine", config_parse_condition,      CONDITION_KERNEL_COMMAND_LINE, &f->suggest_on    },
+                { "Feature", "SuggestOnVersion",           config_parse_condition,      CONDITION_VERSION,             &f->suggest_on    },
+                { "Feature", "SuggestOnCredential",        config_parse_condition,      CONDITION_CREDENTIAL,          &f->suggest_on    },
+                { "Feature", "SuggestOnSecurity",          config_parse_condition,      CONDITION_SECURITY,            &f->suggest_on    },
+                { "Feature", "SuggestOnOSRelease",         config_parse_condition,      CONDITION_OS_RELEASE,          &f->suggest_on    },
+                { "Feature", "SuggestOnMachineTag",        config_parse_condition,      CONDITION_MACHINE_TAG,         &f->suggest_on    },
                 {}
         };
 
@@ -82,3 +100,15 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons
 
         return 0;
 }
+
+int feature_is_suggested(Feature *f) {
+        assert(f);
+
+        if (f->suggest >= 0)
+                return f->suggest;
+
+        if (!f->suggest_on) /* no condition → false */
+                return false;
+
+        return condition_test_list(f->suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL);
+}
index 7d7495087c50f88890ea7ab9cf3d6c0615067325..3f46ded7f9e81d449676f18b7b0600722492c7c2 100644 (file)
@@ -13,6 +13,9 @@ typedef struct Feature {
         char *appstream;
 
         bool enabled;
+
+        int suggest;
+        Condition *suggest_on;
 } Feature;
 
 Feature *feature_new(void);
@@ -23,3 +26,5 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(Feature*, feature_unref);
 extern const struct hash_ops feature_hash_ops;
 
 int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *conf_file_dirs);
+
+int feature_is_suggested(Feature *f);
index 56f02811627befd3993a5eb58a608fea8630a6f3..793a54d4cab9120228b8fc094d3d6df3de06a47c 100644 (file)
@@ -9,6 +9,7 @@
 
 #include "build.h"
 #include "bus-polkit.h"
+#include "condition.h"
 #include "conf-files.h"
 #include "conf-parser.h"
 #include "constants.h"
@@ -38,6 +39,7 @@
 #include "pretty-print.h"
 #include "runtime-scope.h"
 #include "sort-util.h"
+#include "stdio-util.h"
 #include "string-util.h"
 #include "strv.h"
 #include "sysupdate.h"
@@ -87,6 +89,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
                 .cleanup = -1,                                    \
                 .installdb_fd = -EBADF,                           \
                 .target_identifier.class = _TARGET_CLASS_INVALID, \
+                .component_suggest = -1,                          \
         }
 
 void context_done(Context *c) {
@@ -126,6 +129,7 @@ void context_done(Context *c) {
         c->transfer_source = mfree(c->transfer_source);
 
         target_identifier_done(&c->target_identifier);
+        condition_free_list(c->component_suggest_on);
 }
 
 static int context_from_cmdline(Context *ret) {
@@ -346,9 +350,21 @@ static int read_component(Context *c) {
                 return log_oom();
 
         ConfigTableItem table[] = {
-                { "Component", "Description",   config_parse_string,              0, &c->component_description   },
-                { "Component", "Documentation", config_parse_url_specifiers_many, 0, &c->component_documentation },
-                { "Component", "Enabled",       config_parse_bool,                0, &c->component_enabled       },
+                { "Component", "Description",                config_parse_string,              0,                             &c->component_description   },
+                { "Component", "Documentation",              config_parse_url_specifiers_many, 0,                             &c->component_documentation },
+                { "Component", "Enabled",                    config_parse_bool,                0,                             &c->component_enabled       },
+                { "Component", "Suggest",                    config_parse_tristate,            0,                             &c->component_suggest       },
+                { "Component", "SuggestOnArchitecture",      config_parse_condition,           CONDITION_ARCHITECTURE,        &c->component_suggest_on    },
+                { "Component", "SuggestOnFirmware",          config_parse_condition,           CONDITION_FIRMWARE,            &c->component_suggest_on    },
+                { "Component", "SuggestOnVirtualization",    config_parse_condition,           CONDITION_VIRTUALIZATION,      &c->component_suggest_on    },
+                { "Component", "SuggestOnHost",              config_parse_condition,           CONDITION_HOST,                &c->component_suggest_on    },
+                { "Component", "SuggestOnFraction",          config_parse_condition,           CONDITION_FRACTION,            &c->component_suggest_on    },
+                { "Component", "SuggestOnKernelCommandLine", config_parse_condition,           CONDITION_KERNEL_COMMAND_LINE, &c->component_suggest_on    },
+                { "Component", "SuggestOnVersion",           config_parse_condition,           CONDITION_VERSION,             &c->component_suggest_on    },
+                { "Component", "SuggestOnCredential",        config_parse_condition,           CONDITION_CREDENTIAL,          &c->component_suggest_on    },
+                { "Component", "SuggestOnSecurity",          config_parse_condition,           CONDITION_SECURITY,            &c->component_suggest_on    },
+                { "Component", "SuggestOnOSRelease",         config_parse_condition,           CONDITION_OS_RELEASE,          &c->component_suggest_on    },
+                { "Component", "SuggestOnMachineTag",        config_parse_condition,           CONDITION_MACHINE_TAG,         &c->component_suggest_on    },
                 {}
         };
 
@@ -1876,6 +1892,20 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata
                 if (r < 0)
                         return table_log_add_error(r);
 
+                r = feature_is_suggested(f);
+                if (r < 0) {
+                        errno = -r; /* Let's make %m below show this error */
+                        _cleanup_free_ char *k = asprintf_safe("error (%m)");
+                        r = table_add_many(table,
+                                           TABLE_FIELD, "Suggested",
+                                           TABLE_STRING, k);
+                } else
+                        r = table_add_many(table,
+                                           TABLE_FIELD, "Suggested",
+                                           TABLE_BOOLEAN, r);
+                if (r < 0)
+                        return table_log_add_error(r);
+
                 if (f->description) {
                         r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
                         if (r < 0)
@@ -1908,14 +1938,28 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata
 
                 return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
         } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
-                table = table_new("", "feature", "description", "documentation");
+                table = table_new("enabled", "suggested", "feature", "description", "documentation");
                 if (!table)
                         return log_oom();
 
+                table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
                 HASHMAP_FOREACH(f, context.features) {
                         r = table_add_many(table,
                                            TABLE_BOOLEAN_CHECKMARK, f->enabled,
-                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
+                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled));
+                        if (r < 0)
+                                return table_log_add_error(r);
+
+                        r = feature_is_suggested(f);
+                        if (r < 0)
+                                r = table_add_many(table, TABLE_EMPTY);
+                        else
+                                r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, r);
+                        if (r < 0)
+                                return table_log_add_error(r);
+
+                        r = table_add_many(table,
                                            TABLE_STRING, f->id,
                                            TABLE_STRING, f->description,
                                            TABLE_STRING, f->documentation,
@@ -2435,16 +2479,30 @@ static int context_list_components(Context *context, char ***ret_component_names
         return 0;
 }
 
+static int context_component_is_suggested(Context *c) {
+        assert(c);
+
+        /* Only applies to components, not to the main system */
+        if (!c->component)
+                return -ENOTTY;
+
+        if (c->component_suggest >= 0)
+                return c->component_suggest;
+
+        if (!c->component_suggest_on) /* no condition → false */
+                return false;
+
+        return condition_test_list(c->component_suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL);
+}
+
 VERB_NOARG(verb_components, "components",
            "Show list of components");
 static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(context_done) Context context = CONTEXT_NULL;
-        _cleanup_strv_free_ char **component_names = NULL;
-        bool has_default_component = false;
         int r;
 
         assert(argc <= 1);
 
+        _cleanup_(context_done) Context context = CONTEXT_NULL;
         r = context_from_cmdline(&context);
         if (r < 0)
                 return r;
@@ -2459,6 +2517,8 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda
         if (r < 0)
                 return r;
 
+        _cleanup_strv_free_ char **component_names = NULL;
+        bool has_default_component = false;
         r = context_list_components(&context, &component_names, &has_default_component);
         if (r < 0)
                 return log_error_errno(r, "Failed to enumerate components: %m");
@@ -2469,7 +2529,7 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda
                         return 0;
                 }
 
-                _cleanup_(table_unrefp) Table *t = table_new("", "component", "description", "documentation");
+                _cleanup_(table_unrefp) Table *t = table_new("enabled", "suggested", "component", "description", "documentation");
                 if (!t)
                         return log_oom();
 
@@ -2479,6 +2539,7 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda
                         r = table_add_many(
                                         t,
                                         TABLE_EMPTY,
+                                        TABLE_EMPTY,
                                         TABLE_STRING, "<default>",
                                         TABLE_SET_COLOR, ansi_highlight(),
                                         TABLE_STRING, "Default Component",
@@ -2506,12 +2567,25 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda
                         if (r < 0)
                                 continue;
 
+                        r = table_add_many(
+                                        t,
+                                        TABLE_BOOLEAN_CHECKMARK, cc.component_enabled,
+                                        TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled));
+                        if (r < 0)
+                                return table_log_add_error(r);
+
+                        r = context_component_is_suggested(&cc);
+                        if (r < 0)
+                                r = table_add_many(t, TABLE_EMPTY);
+                        else
+                                r = table_add_many(t, TABLE_BOOLEAN_CHECKMARK, r);
+                        if (r < 0)
+                                return table_log_add_error(r);
+
                         const char *doc = cc.component_documentation ? cc.component_documentation[0] : NULL;
 
                         r = table_add_many(
                                         t,
-                                        TABLE_BOOLEAN_CHECKMARK, cc.component_enabled,
-                                        TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled),
                                         TABLE_STRING, *i,
                                         TABLE_STRING, cc.component_description,
                                         TABLE_STRING, doc,
@@ -2850,10 +2924,16 @@ static int vl_server(void) {
 static int run(int argc, char *argv[]) {
         int r;
 
+        LIBAPPARMOR_NOTE(recommended);
+        LIBAUDIT_NOTE(recommended);
         LIBBLKID_NOTE(recommended);
         LIBCRYPTO_NOTE(suggested);
         LIBCRYPTSETUP_NOTE(suggested);
         LIBMOUNT_NOTE(recommended);
+        LIBSELINUX_NOTE(recommended);
+        LIBTSS2_ESYS_NOTE(suggested);
+        LIBTSS2_MU_NOTE(suggested);
+        LIBTSS2_RC_NOTE(suggested);
 
         log_setup();
 
index 6088bde669bff863c652f2d93957dd79dac1d30d..f75f142984b5f9ff1eb5abdfac50907493f01743 100644 (file)
@@ -28,6 +28,9 @@ typedef struct Context {
         char **component_documentation;
         bool component_enabled;
 
+        int component_suggest;
+        Condition *component_suggest_on;
+
         Transfer **transfers;
         size_t n_transfers;