From 40e727c93b5582ed5e1e550c1cd2b5c35683e10e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 17:09:19 +0200 Subject: [PATCH 1/6] sysupdate: add a "suggests" concept to features and components 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 | 47 ++++++++++++++ src/shared/shared-forward.h | 1 + src/sysupdate/sysupdate-config.c | 73 +++++++++++++++++++++ src/sysupdate/sysupdate-config.h | 4 ++ src/sysupdate/sysupdate-feature.c | 38 +++++++++-- src/sysupdate/sysupdate-feature.h | 5 ++ src/sysupdate/sysupdate.c | 102 ++++++++++++++++++++++++++---- src/sysupdate/sysupdate.h | 3 + 8 files changed, 258 insertions(+), 15 deletions(-) diff --git a/man/sysupdate.features.xml b/man/sysupdate.features.xml index 606292e269..d3a7a63810 100644 --- a/man/sysupdate.features.xml +++ b/man/sysupdate.features.xml @@ -109,6 +109,53 @@ + + + Suggest= + + 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 switch of + systemd-sysupdate8) + so that the system administrator or higher-level tooling may act on it. + + If this setting is not specified, the SuggestOn…= conditions described below + are evaluated instead to determine whether the feature is suggested. If neither this setting nor any + SuggestOn…= condition is specified, the feature is not suggested. + + + + + + SuggestOnArchitecture= + SuggestOnFirmware= + SuggestOnVirtualization= + SuggestOnHost= + SuggestOnFraction= + SuggestOnKernelCommandLine= + SuggestOnVersion= + SuggestOnCredential= + SuggestOnSecurity= + SuggestOnOSRelease= + SuggestOnMachineTag= + + Suggest this feature for enablement depending on system properties. These settings + take the same arguments and implement the same semantics — including the leading ! + for negation — as the identically-named + ConditionArchitecture=, ConditionFirmware=, + ConditionVirtualization=, ConditionHost=, + ConditionFraction=, ConditionKernelCommandLine=, + ConditionVersion=, ConditionCredential=, + ConditionSecurity=, ConditionOSRelease= and + ConditionMachineTag= settings for unit files, which are documented in + systemd.unit5. + The feature is suggested if all specified conditions apply. As with the Condition…= + settings, assigning an empty string to one of these resets the list. + + These conditions are only evaluated if Suggest= is not specified. + + + diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index 751a6f71dc..a9c7101e55 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -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; diff --git a/src/sysupdate/sysupdate-config.c b/src/sysupdate/sysupdate-config.c index 7f553a95f0..45794b5a21 100644 --- a/src/sysupdate/sysupdate-config.c +++ b/src/sysupdate/sysupdate-config.c @@ -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; +} diff --git a/src/sysupdate/sysupdate-config.h b/src/sysupdate/sysupdate-config.h index bd972d598d..271430c73d 100644 --- a/src/sysupdate/sysupdate-config.h +++ b/src/sysupdate/sysupdate-config.h @@ -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); diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index 49c2e77988..ef284f7f70 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #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); +} diff --git a/src/sysupdate/sysupdate-feature.h b/src/sysupdate/sysupdate-feature.h index 7d7495087c..3f46ded7f9 100644 --- a/src/sysupdate/sysupdate-feature.h +++ b/src/sysupdate/sysupdate-feature.h @@ -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); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 56f0281162..793a54d4ca 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -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, "", 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(); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index 6088bde669..f75f142984 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -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; From 8f9a2f0c8279cb019406c7f5f07a2c7683780c4c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 17:25:18 +0200 Subject: [PATCH 2/6] sysupdate: introduce --component-suggested similar to --component-all While --component-all really picks all components --component-suggested picks only the suggested ones. Note that none of the verbs currently implement the concept, they will all refuse the option. Hooking this up is going to be added next. --- src/sysupdate/sysupdate.c | 78 ++++++++++++++++++++++----------------- src/sysupdate/sysupdate.h | 10 ++++- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 793a54d4ca..5b37490f91 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -66,7 +66,7 @@ static char *arg_image = NULL; static bool arg_reboot = false; static int arg_cleanup = -1; static char *arg_component = NULL; -static bool arg_component_all = false; +static SelectMode arg_component_select = SELECT_EXPLICIT; static int arg_verify = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_offline = false; @@ -143,7 +143,7 @@ static int context_from_cmdline(Context *ret) { context.verify = arg_verify; context.offline = arg_offline; context.cleanup = arg_cleanup; - context.component_all = arg_component_all; + context.component_select = arg_component_select; if (strdup_to(&context.definitions, arg_definitions) < 0) return log_oom(); @@ -1763,8 +1763,8 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_online( &context, @@ -1839,8 +1839,8 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_offline( &context, @@ -2031,7 +2031,7 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; - if (context.component_all) + if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); r = context_load_offline( @@ -2081,17 +2081,17 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us VERB_NOARG(verb_check_new, "check-new", "Check if there's a new version available"); static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(context_done) Context context = CONTEXT_NULL; int r; assert(argc <= 1); + _cleanup_(context_done) Context context = CONTEXT_NULL; r = context_from_cmdline(&context); if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_online( &context, @@ -2182,7 +2182,6 @@ typedef enum { } UpdateActionFlags; static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) { - _cleanup_(context_done) Context context = CONTEXT_NULL; _cleanup_free_ char *booted_version = NULL; UpdateSet *applied = NULL; const char *version; @@ -2191,16 +2190,17 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag assert(argc <= 2); version = argc >= 2 ? argv[1] : NULL; + _cleanup_(context_done) Context context = CONTEXT_NULL; r = context_from_cmdline(&context); if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); if (context.instances_max < 2) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --instances-max argument must be >= 2 while updating"); + "The --instances-max= argument must be >= 2 while updating"); if (context.reboot) { /* If automatic reboot on completion is requested, let's first determine the currently booted image */ @@ -2309,12 +2309,12 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); if (context.instances_max < 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --instances-max argument must be >= 1 while vacuuming"); + "The --instances-max= argument must be >= 1 while vacuuming"); r = context_load_offline( &context, @@ -2340,6 +2340,9 @@ static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) if (context.cleanup == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invocation of 'cleanup' with --cleanup=no is contradictory, refusing."); + if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-suggested currently not supported for '%s'.", argv[0]); + r = context_load_offline( &context, /* process_image_flags= */ 0, @@ -2350,7 +2353,7 @@ static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) int ret = 0; RET_GATHER(ret, installdb_cleanup_component(&context)); - if (context.component_all) { + if (context.component_select == SELECT_ALL) { _cleanup_strv_free_ char **z = NULL; r = installdb_list_components(&context, &z); if (r < 0) @@ -2402,9 +2405,9 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); - if (context.component || context.component_all) + if (context.component || context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); + "The --component=, --component-all and --component-suggested switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); r = context_load_offline( &context, @@ -2507,8 +2510,11 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); + if (context.definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --definitions= switch may not be combined with '%s'.", argv[0]); r = context_load_offline( &context, @@ -2624,9 +2630,9 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; - if (context.component || context.component_all) + if (context.component || context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= and --component-all switches may not be combined with '%s', " + "The --component= and --component-all/--component-suggested switches may not be combined with '%s', " "specify the component(s) as arguments instead.", argv[0]); if (context.definitions) @@ -2746,7 +2752,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { "Select component to update"): if (isempty(opts.arg)) { arg_component = mfree(arg_component); - arg_component_all = false; + arg_component_select = SELECT_EXPLICIT; break; } @@ -2757,13 +2763,17 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { if (r < 0) return r; - arg_component_all = false; + arg_component_select = SELECT_EXPLICIT; break; - OPTION('A', "component-all", NULL, "Process all components"): - + OPTION('A', "component-all", NULL, "Select all components"): arg_component = mfree(arg_component); - arg_component_all = true; + arg_component_select = SELECT_ALL; + break; + + OPTION('S', "component-suggested", NULL, "Select all suggested components"): + arg_component = mfree(arg_component); + arg_component_select = SELECT_SUGGESTED; break; OPTION_LONG("definitions", "DIR", @@ -2877,11 +2887,13 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with --root= or --image=."); } - if (arg_reboot && arg_component) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=, as automatic reboots only apply to the booted OS version."); + if ((arg_component || arg_component_select != SELECT_EXPLICIT)) { + if (arg_reboot) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=/--component-all/--component-suggested, as automatic reboots only apply to the booted OS version."); - if (arg_definitions && arg_component) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); + if (arg_definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component=/--component-all/--component-suggested switches may not be combined."); + } r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); if (r < 0) diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index f75f142984..f0e25a239d 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -4,6 +4,14 @@ #include "sysupdate-forward.h" #include "sysupdate-target.h" +typedef enum SelectMode { + SELECT_EXPLICIT, /* Only explicitly specified features/components */ + SELECT_ALL, /* All available features/components */ + SELECT_SUGGESTED, /* All suggested features/components */ + _SELECT_MODE_MAX, + _SELECT_MODE_INVALID = -EINVAL, +} SelectMode; + typedef struct Context { /* Parameters/Command line arguments: */ char *definitions; @@ -14,7 +22,7 @@ typedef struct Context { bool reboot; int cleanup; char *component; - bool component_all; + SelectMode component_select; int verify; ImagePolicy *image_policy; bool offline; From 52d9906a54f64e890caf732a910a049f2b224e22 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 17:54:26 +0200 Subject: [PATCH 3/6] sysupdate: implement --component-all and --component-suggested for enable-component and disable-component Let's now introduce "systemd-sysupdate enable-component --component-suggested" and systemd-sysupdate enable-component --component-all" for enabling all or all suggested components at once. Similar, if used for disable-component will disable all components or those not suggested. --- src/sysupdate/sysupdate.c | 103 ++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 5b37490f91..5ecc919ebd 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2617,9 +2617,9 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda return 0; } -VERB(verb_enable_component, "enable-component", "COMPONENT…", 2, VERB_ANY, 0, +VERB(verb_enable_component, "enable-component", "COMPONENT…", 1, VERB_ANY, 0, "Enable component"); -VERB(verb_enable_component, "disable-component", "COMPONENT…", 2, VERB_ANY, 0, +VERB(verb_enable_component, "disable-component", "COMPONENT…", 1, VERB_ANY, 0, "Disable component"); static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void *userdata) { bool enable = streq(argv[0], "enable-component"); @@ -2630,15 +2630,33 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; - if (context.component || context.component_select != SELECT_EXPLICIT) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= and --component-all/--component-suggested switches may not be combined with '%s', " - "specify the component(s) as arguments instead.", argv[0]); - if (context.definitions) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with '%s'.", argv[0]); + char **arguments, *array[2]; + if (argc > 1) { + if (context.component || context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify component names as positional parameter or via --component=/--component-all/--component-suggested, not both."); + + arguments = strv_skip(argv, 1); + + } else if (context.component_select == SELECT_EXPLICIT) { + if (!context.component) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No component specified."); + + array[0] = context.component; + array[1] = NULL; + arguments = array; + } else { + assert(!context.component); + arguments = NULL; + } + + STRV_FOREACH(name, arguments) + if (!component_name_valid(*name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name); + r = context_load_offline( &context, /* process_image_flags= */ 0, @@ -2651,19 +2669,76 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; + _cleanup_strv_free_ char **suggested = NULL; + + switch (context.component_select) { + + case SELECT_EXPLICIT: + STRV_FOREACH(name, arguments) + if (!strv_contains(component_names, *name)) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name); + break; + + case SELECT_ALL: + assert(!arguments); + arguments = component_names; + break; + + case SELECT_SUGGESTED: + assert(!arguments); + STRV_FOREACH(name, component_names) { + _cleanup_(context_done) Context cc = CONTEXT_NULL; + + r = context_from_base_with_component(&context, *name, &cc); + if (r < 0) + return r; + + r = context_load_offline( + &cc, + /* process_image_flags= */ 0, + /* read_definitions_flags= */ 0); + if (r < 0) + continue; + + r = context_component_is_suggested(&cc); + if (r < 0) { + log_warning_errno(r, "Failed to determine whether '%s' shall be enabled, skipping: %m", *name); + continue; + } + + /* This reconciles the system with the suggestions: on 'enable-component' we act on + * the components that are suggested, on 'disable-component' we act on the ones that + * are not. Hence pick the components whose suggestion state matches the operation. */ + if (!!r != !!enable) { + log_debug("Skipping '%s'.", *name); + continue; + } + + log_info("%s '%s'.", enable ? "Enabling" : "Disabling", *name); + + if (strv_extend(&suggested, *name) < 0) + return log_oom(); + } + + arguments = suggested; + break; + + default: + assert_not_reached(); + } + + if (strv_isempty(arguments)) { + log_info("No components selected."); + return 0; + } + /* Component definition files live directly below the configuration directories, hence the drop-in * goes right next to them below /etc. */ _cleanup_free_ char *dropin_dir = path_join(context.root, SYSCONF_DIR); if (!dropin_dir) return log_oom(); - STRV_FOREACH(name, strv_skip(argv, 1)) { - if (!component_name_valid(*name)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name); - - if (!strv_contains(component_names, *name)) - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name); - + STRV_FOREACH(name, arguments) { _cleanup_free_ char *fname = strjoin("sysupdate.", *name, ".component"); if (!fname) return log_oom(); From f23224a7ca7c42c6ac3700110f1dee3225bd367f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 17:55:01 +0200 Subject: [PATCH 4/6] sysupdate: add feature select Prepartion for the next commit: also allow selecting the all or suggested features. --- src/sysupdate/sysupdate.c | 31 +++++++++++++++++++++++++++++++ src/sysupdate/sysupdate.h | 1 + 2 files changed, 32 insertions(+) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 5ecc919ebd..f334756b9f 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -65,6 +65,7 @@ static char *arg_root = NULL; static char *arg_image = NULL; static bool arg_reboot = false; static int arg_cleanup = -1; +static SelectMode arg_feature_select = SELECT_EXPLICIT; static char *arg_component = NULL; static SelectMode arg_component_select = SELECT_EXPLICIT; static int arg_verify = -1; @@ -144,6 +145,7 @@ static int context_from_cmdline(Context *ret) { context.offline = arg_offline; context.cleanup = arg_cleanup; context.component_select = arg_component_select; + context.feature_select = arg_feature_select; if (strdup_to(&context.definitions, arg_definitions) < 0) return log_oom(); @@ -185,6 +187,7 @@ static int context_from_base_with_component(const Context *base, const char *com context.verify = base->verify; context.offline = base->offline; context.cleanup = base->cleanup; + context.feature_select = base->feature_select; if (strdup_to(&context.root, base->root) < 0) return log_oom(); @@ -1763,6 +1766,8 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); @@ -1839,6 +1844,8 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); @@ -2031,6 +2038,8 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); @@ -2090,6 +2099,8 @@ static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdat if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); @@ -2195,6 +2206,8 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); @@ -2309,6 +2322,8 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); @@ -2340,6 +2355,8 @@ static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) if (context.cleanup == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invocation of 'cleanup' with --cleanup=no is contradictory, refusing."); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL)) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-suggested currently not supported for '%s'.", argv[0]); @@ -2405,6 +2422,8 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component || context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --component=, --component-all and --component-suggested switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); @@ -2510,6 +2529,8 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); if (context.definitions) @@ -2630,6 +2651,8 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.definitions) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with '%s'.", argv[0]); @@ -2851,6 +2874,14 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { arg_component_select = SELECT_SUGGESTED; break; + OPTION('a', "feature-all", NULL, "Select all features"): + arg_feature_select = SELECT_ALL; + break; + + OPTION('s', "feature-suggested", NULL, "Select all suggested features"): + arg_feature_select = SELECT_SUGGESTED; + break; + OPTION_LONG("definitions", "DIR", "Find transfer definitions in specified directory"): r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index f0e25a239d..1930367093 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -23,6 +23,7 @@ typedef struct Context { int cleanup; char *component; SelectMode component_select; + SelectMode feature_select; int verify; ImagePolicy *image_policy; bool offline; From 9f3b604d6422513a703b5051c3d23af34be960a0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 30 Jun 2026 21:46:07 +0200 Subject: [PATCH 5/6] sysupdate: validate feature name syntax before using it Let's tighten the rules on features, and enforce a sensible naming policy. --- src/sysupdate/sysupdate-feature.c | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index ef284f7f70..26e0564a97 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -10,6 +10,7 @@ #include "string-util.h" #include "sysupdate-config.h" #include "sysupdate-feature.h" +#include "sysupdate-util.h" static Feature *feature_free(Feature *f) { if (!f) @@ -48,7 +49,27 @@ DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops, Feature, feature_unref); int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *dirs) { + int r; + assert(f); + assert(path); + assert(dirs); + + _cleanup_free_ char *filename = NULL; + r = path_extract_filename(path, &filename); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from path '%s': %m", path); + + char *e = endswith(filename, ".feature"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature file has bad suffix: %s", filename); + + _cleanup_free_ char *id = strndup(filename, e - filename); + if (!id) + return log_oom(); + + if (!feature_name_valid(id)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed feature filename '%s': %m", filename); ConfigTableItem table[] = { { "Feature", "Description", config_parse_string, 0, &f->description }, @@ -70,16 +91,6 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons {} }; - _cleanup_free_ char *filename = NULL; - int r; - - assert(path); - assert(dirs); - - r = path_extract_filename(path, &filename); - if (r < 0) - return log_error_errno(r, "Failed to extract filename from path '%s': %m", path); - r = config_parse_many_full( STRV_MAKE_CONST(path), dirs, @@ -95,8 +106,7 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons if (r < 0) return r; - *ASSERT_PTR(endswith(filename, ".feature")) = 0; /* Remove the file extension */ - f->id = TAKE_PTR(filename); + f->id = TAKE_PTR(id); return 0; } From 4481661a75acc01b5d66aa36443a7b80b557e4ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 30 Jun 2026 21:46:46 +0200 Subject: [PATCH 6/6] sysupdate: add support for --component-all + --feature-all + --feature-suggested to enable-feature verb This adds similar concepts as we already have for enable-component: let's add a way to enable all features or the suggeste dones, and possibly on all components. Or in other words, with this: systemd-sysupdate enable-component -S systemd-sysupdate enable-feature -A -s We'll automatically enable all suggested components, and all features of them. --- src/sysupdate/sysupdate.c | 202 +++++++++++++++++++++++++++++++------- 1 file changed, 167 insertions(+), 35 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index f334756b9f..01b4a03f4d 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2025,9 +2025,114 @@ static int make_dropin_dir(Context *c, char **ret) { return 0; } -VERB(verb_enable_feature, "enable-feature", "FEATURE…", 2, VERB_ANY, 0, +static const char *context_component_display(const Context *c) { + assert(c); + + return c->component ?: ""; +} + +static int context_enable_feature( + Context *c, + const char *argv0, + bool enable, + char **features) { + + int r; + + assert(c); + assert(argv0); + + _cleanup_free_ char **l = NULL; + switch (c->feature_select) { + + case SELECT_EXPLICIT: + break; + + case SELECT_ALL: { + assert(!features); + + Feature *f; + HASHMAP_FOREACH(f, c->features) + if (strv_push(&l, f->id) < 0) + return log_oom(); + + features = l; + break; + } + + case SELECT_SUGGESTED: { + assert(!features); + + Feature *f; + HASHMAP_FOREACH(f, c->features) { + r = feature_is_suggested(f); + if (r < 0) + return log_error_errno(r, "Failed to determine if feature '%s' of component '%s' shall be enabled: %m", f->id, context_component_display(c)); + if (!!r != !!enable) { + log_debug("Skipping feature '%s' of component '%s'.", f->id, context_component_display(c)); + continue; + } + + log_info("%s feature '%s' of component '%s'.", enable ? "Enabling" : "Disabling", f->id, context_component_display(c)); + + if (strv_push(&l, f->id) < 0) + return log_oom(); + } + + features = l; + break; + } + + default: + assert_not_reached(); + } + + if (strv_isempty(features)) { + log_debug("No features selected."); + return 0; + } + + _cleanup_free_ char *dropin_dir = NULL; + r = make_dropin_dir(c, &dropin_dir); + if (r < 0) + return r; + + int ret = 0; + STRV_FOREACH(name, features) { + if (!hashmap_contains(c->features, *name)) { + RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Optional feature not found in component '%s': %s", context_component_display(c), *name)); + continue; + } + + _cleanup_free_ char *fname = strjoin(*name, ".feature"); + if (!fname) + return log_oom(); + + /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */ + r = write_drop_in_format( + dropin_dir, + fname, + 50, + "systemd-sysupdate-enabled", + "# Generated via 'systemd-sysupdate %s'\n\n" + "[Feature]\n" + "Enabled=%s\n", + argv0, + yes_no(enable)); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name)); + continue; + } + + log_info("Feature '%s' %s.", *name, enabled_disabled(enable)); + } + + return ret; +} + +VERB(verb_enable_feature, "enable-feature", "FEATURE…", 1, VERB_ANY, 0, "Enable optional feature"); -VERB(verb_enable_feature, "disable-feature", "FEATURE…", 2, VERB_ANY, 0, +VERB(verb_enable_feature, "disable-feature", "FEATURE…", 1, VERB_ANY, 0, "Disable optional feature"); static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *userdata) { bool enable = streq(argv[0], "enable-feature"); @@ -2038,10 +2143,24 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; - if (context.feature_select != SELECT_EXPLICIT) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); - if (context.component_select != SELECT_EXPLICIT) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--component-suggested is not supported for '%s'.", argv[0]); + if (context.definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --definitions= switch may not be combined with '%s'.", argv[0]); + + char **features; + if (argc > 1 && context.feature_select == SELECT_EXPLICIT) { + features = strv_skip(argv, 1); + + STRV_FOREACH(name, features) + if (!feature_name_valid(*name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name); + + } else if (argc <= 1 && context.feature_select != SELECT_EXPLICIT) + features = NULL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify features as positional parameter or via --feature-all/--feature-suggested, not both."); r = context_load_offline( &context, @@ -2050,41 +2169,54 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; - _cleanup_free_ char *dropin_dir = NULL; - r = make_dropin_dir(&context, &dropin_dir); - if (r < 0) - return r; + switch (context.component_select) { - STRV_FOREACH(name, strv_skip(argv, 1)) { + case SELECT_EXPLICIT: + return context_enable_feature(&context, argv[0], enable, features); - if (!feature_name_valid(*name)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name); - - if (!hashmap_contains(context.features, *name)) - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), - "Optional feature not found: %s", *name); - - _cleanup_free_ char *fname = strjoin(*name, ".feature"); - if (!fname) - return log_oom(); - - /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */ - r = write_drop_in_format( - dropin_dir, - fname, - 50, "systemd-sysupdate-enabled", - "# Generated via 'systemd-sysupdate %s'\n\n" - "[Feature]\n" - "Enabled=%s\n", - argv[0], - yes_no(enable)); + case SELECT_ALL: { + _cleanup_strv_free_ char **component_names = NULL; + bool has_default_component; + r = context_list_components(&context, &component_names, &has_default_component); if (r < 0) - return log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name); + return r; - log_info("Feature '%s' %s.", *name, enable ? "enabled" : "disabled"); + if (strv_isempty(component_names) && !has_default_component) { + log_debug("No components selected."); + return 0; + } + + int ret = 0; + if (has_default_component) + RET_GATHER(ret, context_enable_feature(&context, argv[0], enable, features)); + + STRV_FOREACH(name, component_names) { + _cleanup_(context_done) Context cc = CONTEXT_NULL; + + r = context_from_base_with_component(&context, *name, &cc); + if (r < 0) { + RET_GATHER(ret, r); + continue; + } + + r = context_load_offline( + &cc, + /* process_image_flags= */ 0, + /* read_definitions_flags= */ 0); + if (r < 0) { + RET_GATHER(ret, r); + continue; + } + + RET_GATHER(ret, context_enable_feature(&cc, argv[0], enable, features)); + } + + return ret; } - return 0; + default: + assert_not_reached(); + } } VERB_NOARG(verb_check_new, "check-new",