From ec10efa1a1af1881adb89d71eaf3916d61c73b96 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 17 Nov 2025 15:14:19 +0000 Subject: [PATCH 01/11] sysupdate: Split context_apply() into acquire and install steps This introduces no functional changes at the moment, but will be used in upcoming commits. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate.c | 47 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 05163754af..e1143ad4ce 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -982,10 +982,9 @@ static int context_on_acquire_progress(const Transfer *t, const Instance *inst, overall, n - i, i, inst->metadata.version, overall); } -static int context_apply( +static int context_acquire( Context *c, - const char *version, - UpdateSet **ret_applied) { + const char *version) { UpdateSet *us = NULL; int r; @@ -1000,9 +999,6 @@ static int context_apply( if (!c->candidate) { log_info("No update needed."); - if (ret_applied) - *ret_applied = NULL; - return 0; } @@ -1014,9 +1010,6 @@ static int context_apply( else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { log_info("Selected update '%s' is already installed. Skipping update.", us->version); - if (ret_applied) - *ret_applied = NULL; - return 0; } @@ -1075,7 +1068,34 @@ static int context_apply( if (arg_sync) sync(); - (void) sd_notifyf(/* unset_environment= */ false, + return 1; +} + +static int context_install( + Context *c, + const char *version, + UpdateSet **ret_applied) { + + UpdateSet *us = NULL; + int r; + + assert(c); + + if (version) { + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + } else { + if (!c->candidate) { + log_info("No update needed."); + + return 0; + } + + us = c->candidate; + } + + (void) sd_notifyf(/* unset_environment=*/ false, "STATUS=Installing '%s'.", us->version); for (size_t i = 0; i < c->n_transfers; i++) { @@ -1443,7 +1463,12 @@ static int verb_update(int argc, char **argv, void *userdata) { if (r < 0) return r; - r = context_apply(context, version, &applied); + r = context_acquire(context, version); + if (r < 0) + return r; /* error */ + + if (r > 0) /* update needed */ + r = context_install(context, version, &applied); if (r < 0) return r; From fdc661116d47797804b8042db91effc0c1e8fdd0 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 30 Dec 2025 23:49:47 +0000 Subject: [PATCH 02/11] sysupdate: Allow instances to be partial or pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we allow target instances to be partial or pending, we can build on top of this to allow updates to be split into two phases: ‘acquire’ (which takes an available source instance and copies it (temporarily partial) to a pending target instance; and ‘install’ (which takes a pending target instance and installs it as an installed target instance). This commit introduces a file/directory and partition prefix naming scheme to identify partial and pending instances. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-instance.h | 5 ++- src/sysupdate/sysupdate-resource.c | 57 +++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h index 42ba1f1ba9..05b42ac610 100644 --- a/src/sysupdate/sysupdate-instance.h +++ b/src/sysupdate/sysupdate-instance.h @@ -45,8 +45,11 @@ struct Instance { InstanceMetadata metadata; /* Where we found the instance */ - char *path; + char *path; /* includes the `.sysupdate.partial.` (etc.) prefix, if applicable */ PartitionInfo partition_info; + + bool is_partial; + bool is_pending; }; void instance_metadata_destroy(InstanceMetadata *m); diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c index b268841899..821854cda8 100644 --- a/src/sysupdate/sysupdate-resource.c +++ b/src/sysupdate/sysupdate-resource.c @@ -80,15 +80,22 @@ static int resource_load_from_directory_recursive( Resource *rr, DIR* d, const char* relpath, - mode_t m) { + const char* relpath_for_matching, + mode_t m, + bool ancestor_is_partial, + bool ancestor_is_pending) { int r; for (;;) { _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; _cleanup_free_ char *joined = NULL, *rel_joined = NULL; + _cleanup_free_ char *rel_joined_for_matching = NULL; Instance *instance; struct dirent *de; + const char *de_d_name_stripped; struct stat st; + bool is_partial = ancestor_is_partial, is_pending = ancestor_is_pending; + const char *stripped; errno = 0; de = readdir_no_dot(d); @@ -128,11 +135,26 @@ static int resource_load_from_directory_recursive( if (!(S_ISDIR(st.st_mode) && S_ISREG(m)) && ((st.st_mode & S_IFMT) != m)) continue; + if ((stripped = startswith(de->d_name, ".sysupdate.partial."))) { + de_d_name_stripped = stripped; + is_partial = true; + } else if ((stripped = startswith(de->d_name, ".sysupdate.pending."))) { + de_d_name_stripped = stripped; + is_pending = true; + } else + de_d_name_stripped = de->d_name; + rel_joined = path_join(relpath, de->d_name); if (!rel_joined) return log_oom(); - r = pattern_match_many(rr->patterns, rel_joined, &extracted_fields); + /* Match against the filename with any `.sysupdate.partial.` (etc.) prefix stripped, so the + * user’s patterns still apply. But don’t use the stripped version in any paths or recursion. */ + rel_joined_for_matching = path_join(relpath_for_matching, de_d_name_stripped); + if (!rel_joined_for_matching) + return log_oom(); + + r = pattern_match_many(rr->patterns, rel_joined_for_matching, &extracted_fields); if (r == PATTERN_MATCH_RETRY) { _cleanup_closedir_ DIR *subdir = NULL; @@ -140,7 +162,7 @@ static int resource_load_from_directory_recursive( if (!subdir) continue; - r = resource_load_from_directory_recursive(rr, subdir, rel_joined, m); + r = resource_load_from_directory_recursive(rr, subdir, rel_joined, rel_joined_for_matching, m, is_partial, is_pending); if (r < 0) return r; if (r == 0) @@ -168,6 +190,9 @@ static int resource_load_from_directory_recursive( if (instance->metadata.mode == MODE_INVALID) instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */ + + instance->is_partial = is_partial; + instance->is_pending = is_pending; } return 0; @@ -192,7 +217,7 @@ static int resource_load_from_directory( return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path); } - return resource_load_from_directory_recursive(rr, d, NULL, m); + return resource_load_from_directory_recursive(rr, d, NULL, NULL, m, false, false); } static int resource_load_from_blockdev(Resource *rr) { @@ -219,6 +244,9 @@ static int resource_load_from_blockdev(Resource *rr) { _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL; Instance *instance; + const char *pinfo_label_stripped; + bool is_partial = false, is_pending = false; + const char *stripped; r = read_partition_info(c, t, i, &pinfo); if (r < 0) @@ -236,7 +264,18 @@ static int resource_load_from_blockdev(Resource *rr) { continue; } - r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields); + /* Match the label with any partial/pending prefix removed so the user’s existing patterns + * match regardless of the instance’s state. */ + if ((stripped = startswith(pinfo.label, "PRT#"))) { + pinfo_label_stripped = stripped; + is_partial = true; + } else if ((stripped = startswith(pinfo.label, "PND#"))) { + pinfo_label_stripped = stripped; + is_pending = true; + } else + pinfo_label_stripped = pinfo.label; + + r = pattern_match_many(rr->patterns, pinfo_label_stripped, &extracted_fields); if (r < 0) return log_error_errno(r, "Failed to match pattern: %m"); if (IN_SET(r, PATTERN_MATCH_NO, PATTERN_MATCH_RETRY)) @@ -262,6 +301,9 @@ static int resource_load_from_blockdev(Resource *rr) { if (instance->metadata.read_only < 0) instance->metadata.read_only = instance->partition_info.read_only; + + instance->is_partial = is_partial; + instance->is_pending = is_pending; } return 0; @@ -539,6 +581,11 @@ static int resource_load_from_web( memcpy(instance->metadata.sha256sum, h.iov_base, h.iov_len); instance->metadata.sha256sum_set = true; } + + /* Web resources can only be a source, not a target, so + * can never be partial or pending. */ + instance->is_partial = false; + instance->is_pending = false; } } From edf7679faf98a20687dacc928d0c3f9fecccbac2 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 30 Dec 2025 23:56:22 +0000 Subject: [PATCH 03/11] sysupdate: Factor out a vacuum helper function This will be reused in an upcoming commit. This commit introduces no functional changes. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-transfer.c | 75 ++++++++++++++++++------------ 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 51ae49f407..0d017f9043 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -740,6 +740,48 @@ static void transfer_remove_temporary(Transfer *t) { } } +static int transfer_instance_vacuum( + Transfer *t, + Instance *instance) { + int r; + + assert(t); + assert(instance); + + switch (t->target.type) { + + case RESOURCE_REGULAR_FILE: + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + r = rm_rf(instance->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", instance->path); + + (void) rmdir_parents(instance->path, t->target.path); + + break; + + case RESOURCE_PARTITION: { + PartitionInfo pinfo = instance->partition_info; + + /* label "_empty" means "no contents" for our purposes */ + pinfo.label = (char*) "_empty"; + + r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL); + if (r < 0) + return r; + + t->target.n_empty++; + break; + } + + default: + assert_not_reached(); + } + + return 0; +} + int transfer_vacuum( Transfer *t, uint64_t space, @@ -840,36 +882,9 @@ int transfer_vacuum( oldest->path, resource_type_to_string(oldest->resource->type)); - switch (t->target.type) { - - case RESOURCE_REGULAR_FILE: - case RESOURCE_DIRECTORY: - case RESOURCE_SUBVOLUME: - r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD); - if (r < 0 && r != -ENOENT) - return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path); - - (void) rmdir_parents(oldest->path, t->target.path); - - break; - - case RESOURCE_PARTITION: { - PartitionInfo pinfo = oldest->partition_info; - - /* label "_empty" means "no contents" for our purposes */ - pinfo.label = (char*) "_empty"; - - r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL); - if (r < 0) - return r; - - t->target.n_empty++; - break; - } - - default: - assert_not_reached(); - } + r = transfer_instance_vacuum(t, oldest); + if (r < 0) + return 0; instance_free(oldest); memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*)); From 48fff6a27aab434035d5ead65d48a6f860f5b656 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:02:06 +0000 Subject: [PATCH 04/11] sysupdate: Implement acquire and install steps for transfers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of using a random temporary path for file transfers, use a predictable one which indicates whether the transfer is partially complete or pending installation. Similarly for partitions. This is another step towards being able to split the ‘update’ step into ‘acquire’ and ‘install’. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-transfer.c | 154 ++++++++++++++++++++++++----- src/sysupdate/sysupdate-transfer.h | 6 +- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 0d017f9043..6d1262c161 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -41,7 +41,6 @@ #include "sysupdate-resource.h" #include "sysupdate-transfer.h" #include "time-util.h" -#include "tmpfile-util.h" #include "web-util.h" /* Default value for InstancesMax= for fs object targets */ @@ -51,7 +50,8 @@ Transfer* transfer_free(Transfer *t) { if (!t) return NULL; - t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path); + free(t->temporary_partial_path); + free(t->temporary_pending_path); free(t->id); @@ -67,6 +67,9 @@ Transfer* transfer_free(Transfer *t) { strv_free(t->appstream); partition_info_destroy(&t->partition_info); + free(t->temporary_partial_partition_label); + free(t->temporary_pending_partition_label); + free(t->final_partition_label); resource_destroy(&t->source); resource_destroy(&t->target); @@ -767,6 +770,7 @@ static int transfer_instance_vacuum( /* label "_empty" means "no contents" for our purposes */ pinfo.label = (char*) "_empty"; + log_debug("Relabelling partition '%s' to '%s'.", pinfo.device, pinfo.label); r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL); if (r < 0) return r; @@ -1128,6 +1132,8 @@ static int run_callout( int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) { _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL; + _cleanup_free_ char *formatted_partial_pattern = NULL; + _cleanup_free_ char *formatted_pending_pattern = NULL; char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1]; const char *where = NULL; InstanceMetadata f; @@ -1147,7 +1153,11 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi } assert(!t->final_path); - assert(!t->temporary_path); + assert(!t->temporary_partial_path); + assert(!t->temporary_pending_path); + assert(!t->final_partition_label); + assert(!t->temporary_partial_partition_label); + assert(!t->temporary_pending_partition_label); assert(!strv_isempty(t->target.patterns)); /* Format the target name using the first pattern specified */ @@ -1157,6 +1167,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi return log_error_errno(r, "Failed to format target pattern: %m"); if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + _cleanup_free_ char *final_dir = NULL, *final_filename = NULL, *partial_filename = NULL, *pending_filename = NULL; if (!path_is_safe(formatted_pattern)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern); @@ -1169,9 +1180,37 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi if (r < 0) return log_error_errno(r, "Cannot create target directory: %m"); - r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path); + /* Build the paths for the partial and pending files, which hold the resource while it’s + * being acquired and after it’s been acquired (but before it’s moved to the final_path + * when it’s installed). + * + * Split the filename off the `final_path`, then add a prefix to it for each of partial and + * pending, then join them back on to the same directory. */ + r = path_split_prefix_filename(t->final_path, &final_dir, &final_filename); if (r < 0) - return log_error_errno(r, "Failed to generate temporary target path: %m"); + return log_error_errno(r, "Failed to parse path: %m"); + + if (!strprepend(&partial_filename, ".sysupdate.partial.", final_filename)) + return log_oom(); + + if (!strprepend(&pending_filename, ".sysupdate.pending.", final_filename)) + return log_oom(); + + t->temporary_partial_path = path_join(final_dir, partial_filename); + if (!t->temporary_partial_path) + return log_oom(); + + r = mkdir_parents(t->temporary_partial_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); + + t->temporary_pending_path = path_join(final_dir, pending_filename); + if (!t->temporary_pending_path) + return log_oom(); + + r = mkdir_parents(t->temporary_pending_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); where = t->final_path; } @@ -1183,6 +1222,28 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi if (!r) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern); + if (!strprepend(&formatted_partial_pattern, "PRT#", formatted_pattern)) + return log_oom(); + r = gpt_partition_label_valid(formatted_partial_pattern); + if (r < 0) + return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_partial_pattern); + if (!r) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_partial_pattern); + + free_and_replace(t->temporary_partial_partition_label, formatted_partial_pattern); + + if (!strprepend(&formatted_pending_pattern, "PND#", formatted_pattern)) + return log_oom(); + r = gpt_partition_label_valid(formatted_pending_pattern); + if (r < 0) + return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pending_pattern); + if (!r) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pending_pattern); + + free_and_replace(t->temporary_pending_partition_label, formatted_pending_pattern); + + t->final_partition_label = TAKE_PTR(formatted_pattern); + r = find_suitable_partition( t->target.path, i->metadata.size, @@ -1195,6 +1256,20 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi xsprintf(max_size, "%" PRIu64, t->partition_info.size); where = t->partition_info.device; + + /* Rename the partition to `PRT#` to indicate that a transfer to it is in progress. */ + r = free_and_strdup_warn(&t->partition_info.label, t->temporary_partial_partition_label); + if (r < 0) + return r; + t->partition_change = PARTITION_LABEL; + + log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label); + r = patch_partition( + t->target.path, + &t->partition_info, + t->partition_change); + if (r < 0) + return r; } assert(where); @@ -1233,7 +1308,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi "--direct", /* just copy/unpack the specified file, don't do anything else */ arg_sync ? "--sync=yes" : "--sync=no", i->path, - t->temporary_path), + t->temporary_partial_path), t, i, cb, userdata); break; @@ -1274,7 +1349,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi arg_sync ? "--sync=yes" : "--sync=no", t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", i->path, - t->temporary_path), + t->temporary_partial_path), t, i, cb, userdata); break; @@ -1291,7 +1366,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi arg_sync ? "--sync=yes" : "--sync=no", t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", i->path, - t->temporary_path), + t->temporary_partial_path), t, i, cb, userdata); break; @@ -1311,7 +1386,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi "--verify", digest, /* validate by explicit SHA256 sum */ arg_sync ? "--sync=yes" : "--sync=no", i->path, - t->temporary_path), + t->temporary_partial_path), t, i, cb, userdata); break; @@ -1351,7 +1426,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", arg_sync ? "--sync=yes" : "--sync=no", i->path, - t->temporary_path), + t->temporary_partial_path), t, i, cb, userdata); break; @@ -1363,7 +1438,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi if (RESOURCE_IS_FILESYSTEM(t->target.type)) { bool need_sync = false; - assert(t->temporary_path); + assert(t->temporary_partial_path); + assert(t->temporary_pending_path); /* Apply file attributes if set */ if (f.mtime != USEC_INFINITY) { @@ -1371,8 +1447,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi timespec_store(&ts, f.mtime); - if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0) - return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path); + if (utimensat(AT_FDCWD, t->temporary_partial_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0) + return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_partial_path); need_sync = true; } @@ -1381,9 +1457,9 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi /* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older * kernels don't support that however, in that case we fall back to chmod(). Not as * safe, but shouldn't be a problem, given that we don't create symlinks here. */ - if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 && - (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0)) - return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path); + if (fchmodat(AT_FDCWD, t->temporary_partial_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 && + (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_partial_path, f.mode) < 0)) + return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_partial_path); need_sync = true; } @@ -1391,20 +1467,34 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi /* Synchronize */ if (arg_sync && need_sync) { if (t->target.type == RESOURCE_REGULAR_FILE) - r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path); + r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_partial_path); else { assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); - r = syncfs_path(AT_FDCWD, t->temporary_path); + r = syncfs_path(AT_FDCWD, t->temporary_partial_path); } if (r < 0) - return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path); + return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_partial_path); } t->install_read_only = f.read_only; + + /* Rename the file from `.sysupdate.partial.` to `.sysupdate.pending.` to indicate it’s ready to install. */ + log_debug("Renaming resource instance '%s' to '%s'.", t->temporary_partial_path, t->temporary_pending_path); + r = install_file(AT_FDCWD, t->temporary_partial_path, + AT_FDCWD, t->temporary_pending_path, + INSTALL_REPLACE| + (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)| + (t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS)); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into pending place: %m", t->temporary_pending_path); } if (t->target.type == RESOURCE_PARTITION) { - free_and_replace(t->partition_info.label, formatted_pattern); + /* Now rename the partition again to `PND#` to indicate that the acquire is complete + * and the partition is ready for install. */ + r = free_and_strdup_warn(&t->partition_info.label, t->temporary_pending_partition_label); + if (r < 0) + return r; t->partition_change = PARTITION_LABEL; if (f.partition_uuid_set) { @@ -1431,6 +1521,14 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi t->partition_info.growfs = f.growfs; t->partition_change |= PARTITION_GROWFS; } + + log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label); + r = patch_partition( + t->target.path, + &t->partition_info, + t->partition_change); + if (r < 0) + return r; } /* For regular file cases the only step left is to install the file in place, which install_file() @@ -1451,13 +1549,13 @@ int transfer_install_instance( assert(t); assert(i); assert(i->resource); - assert(t == container_of(i->resource, Transfer, source)); + assert(i->is_pending || t == container_of(i->resource, Transfer, source)); - if (t->temporary_path) { + if (t->temporary_pending_path) { assert(RESOURCE_IS_FILESYSTEM(t->target.type)); assert(t->final_path); - r = install_file(AT_FDCWD, t->temporary_path, + r = install_file(AT_FDCWD, t->temporary_pending_path, AT_FDCWD, t->final_path, INSTALL_REPLACE| (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)| @@ -1471,11 +1569,17 @@ int transfer_install_instance( t->final_path, resource_type_to_string(t->target.type)); - t->temporary_path = mfree(t->temporary_path); + t->temporary_pending_path = mfree(t->temporary_pending_path); } - if (t->partition_change != 0) { + if (t->temporary_pending_partition_label) { assert(t->target.type == RESOURCE_PARTITION); + assert(t->final_partition_label); + + r = free_and_strdup_warn(&t->partition_info.label, t->final_partition_label); + if (r < 0) + return r; + t->partition_change = PARTITION_LABEL; r = patch_partition( t->target.path, diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h index 1a6cba804f..92c1922795 100644 --- a/src/sysupdate/sysupdate-transfer.h +++ b/src/sysupdate/sysupdate-transfer.h @@ -39,13 +39,17 @@ typedef struct Transfer { int growfs; /* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */ - char *temporary_path; + char *temporary_partial_path; + char *temporary_pending_path; char *final_path; int install_read_only; /* If we write to a partition in a partition table, the metrics of it */ PartitionInfo partition_info; PartitionChange partition_change; + char *final_partition_label; + char *temporary_partial_partition_label; + char *temporary_pending_partition_label; Context *context; } Transfer; From adcc03a0c027abcc8e4c759885116942245612d2 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:05:05 +0000 Subject: [PATCH 05/11] sysupdate: Vacuum partial/pending instances first Modify the vacuum implementation to preferentially vacuum partial or pending transfers first (unless protected) as they are meant to be fairly transitory, and ones which are hanging around have probably been forgotten about and/or are out of date. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-transfer.c | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 6d1262c161..f624a50cf4 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -798,7 +798,45 @@ int transfer_vacuum( transfer_remove_temporary(t); - /* First, calculate how many instances to keep, based on the instance limit — but keep at least one */ + /* First, remove any partial or pending instances (unless protected) */ + for (size_t i = 0; i < t->target.n_instances;) { + Instance *instance = t->target.instances[i]; + + assert(instance); + + if (!instance->is_pending && !instance->is_partial) { + i++; + continue; + } + + /* If this is listed among the protected versions, then let's not remove it */ + if (strv_contains(t->protected_versions, instance->metadata.version) || + (extra_protected_version && streq(extra_protected_version, instance->metadata.version))) { + log_debug("Version '%s' is pending/partial but protected, not removing.", instance->metadata.version); + i++; + continue; + } + + assert(instance->resource); + + log_info("%s Removing old %s '%s' (%s).", + glyph(GLYPH_RECYCLING), + instance->is_partial ? "partial" : "pending", + instance->path, + resource_type_to_string(instance->resource->type)); + + r = transfer_instance_vacuum(t, instance); + if (r < 0) + return 0; + + instance_free(instance); + memmove(t->target.instances + i, t->target.instances + i + 1, (t->target.n_instances - i - 1) * sizeof(Instance*)); + t->target.n_instances--; + + count++; + } + + /* Second, calculate how many instances to keep, based on the instance limit — but keep at least one */ instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max; assert(instances_max >= 1); From ba9687adefa46e1b0f1f111248e74cd7dec75ff6 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:36:22 +0000 Subject: [PATCH 06/11] sysupdate: Factor out temporary path computation for transfers This helper function will be reused in a following commit. This introduces no functional changes. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-transfer.c | 88 +++++++++++++++++++----------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index f624a50cf4..5af2f4cf30 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -1168,27 +1168,15 @@ static int run_callout( return sd_event_loop(event); } -int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) { - _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL; - _cleanup_free_ char *formatted_partial_pattern = NULL; - _cleanup_free_ char *formatted_pending_pattern = NULL; - char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1]; - const char *where = NULL; - InstanceMetadata f; - Instance *existing; +/* Build the filenames and paths which is normally done by transfer_acquire_instance(), but for partial + * and pending instances which are about to be installed (in which case, transfer_acquire_instance() is + * skipped). */ +static int transfer_compute_temporary_paths(Transfer *t, Instance *i, InstanceMetadata *f) { + _cleanup_free_ char *formatted_pattern = NULL, *formatted_partial_pattern = NULL, *formatted_pending_pattern = NULL; int r; assert(t); assert(i); - assert(i->resource == &t->source); - assert(cb); - - /* Does this instance already exist in the target? Then we don't need to acquire anything */ - existing = resource_find_instance(&t->target, i->metadata.version); - if (existing) { - log_info("No need to acquire '%s', already installed.", i->path); - return 0; - } assert(!t->final_path); assert(!t->temporary_partial_path); @@ -1199,8 +1187,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi assert(!strv_isempty(t->target.patterns)); /* Format the target name using the first pattern specified */ - compile_pattern_fields(t, i, &f); - r = pattern_format(t->target.patterns[0], &f, &formatted_pattern); + compile_pattern_fields(t, i, f); + r = pattern_format(t->target.patterns[0], f, &formatted_pattern); if (r < 0) return log_error_errno(r, "Failed to format target pattern: %m"); @@ -1214,10 +1202,6 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi if (!t->final_path) return log_oom(); - r = mkdir_parents(t->final_path, 0755); - if (r < 0) - return log_error_errno(r, "Cannot create target directory: %m"); - /* Build the paths for the partial and pending files, which hold the resource while it’s * being acquired and after it’s been acquired (but before it’s moved to the final_path * when it’s installed). @@ -1238,19 +1222,9 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi if (!t->temporary_partial_path) return log_oom(); - r = mkdir_parents(t->temporary_partial_path, 0755); - if (r < 0) - return log_error_errno(r, "Cannot create target directory: %m"); - t->temporary_pending_path = path_join(final_dir, pending_filename); if (!t->temporary_pending_path) return log_oom(); - - r = mkdir_parents(t->temporary_pending_path, 0755); - if (r < 0) - return log_error_errno(r, "Cannot create target directory: %m"); - - where = t->final_path; } if (t->target.type == RESOURCE_PARTITION) { @@ -1281,7 +1255,55 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi free_and_replace(t->temporary_pending_partition_label, formatted_pending_pattern); t->final_partition_label = TAKE_PTR(formatted_pattern); + } + return 0; +} + +int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) { + _cleanup_free_ char *digest = NULL; + char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1]; + const char *where = NULL; + InstanceMetadata f; + Instance *existing; + int r; + + assert(t); + assert(i); + assert(i->resource == &t->source); + assert(cb); + + /* Does this instance already exist in the target? Then we don't need to acquire anything */ + existing = resource_find_instance(&t->target, i->metadata.version); + if (existing && (existing->is_partial || existing->is_pending)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already partial or pending in the target.", i->path); + if (existing) { + log_info("No need to acquire '%s', already installed.", i->path); + return 0; + } + + /* Compute up the temporary paths */ + r = transfer_compute_temporary_paths(t, i, &f); + if (r < 0) + return r; + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + r = mkdir_parents(t->temporary_partial_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); + + r = mkdir_parents(t->temporary_pending_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); + + r = mkdir_parents(t->final_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); + + where = t->final_path; + } + + if (t->target.type == RESOURCE_PARTITION) { r = find_suitable_partition( t->target.path, i->metadata.size, From 594d0345fa997446b4c2dcfbccf3f83257bb55a3 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:42:17 +0000 Subject: [PATCH 07/11] sysupdate: Add partial/pending flags to UpdateSet This commit adds the flags and some basic formatting/printing of them. Following commits will integrate them into the update/acquire/install logic. `UPDATE_PARTIAL` is set if any of the instances in the `UpdateSet` are partial, i.e. have been partially downloaded. `UPDATE_PENDING` is set if any of the instances in the `UpdateSet` are pending, i.e. have been acquired (downloaded) but not yet installed. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-update-set-flags.c | 18 ++++++++++++++++++ src/sysupdate/sysupdate-update-set-flags.h | 2 ++ src/sysupdate/sysupdate.c | 15 +++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/sysupdate/sysupdate-update-set-flags.c b/src/sysupdate/sysupdate-update-set-flags.c index 394e390791..36801938f6 100644 --- a/src/sysupdate/sysupdate-update-set-flags.c +++ b/src/sysupdate/sysupdate-update-set-flags.c @@ -9,6 +9,9 @@ const char* update_set_flags_to_color(UpdateSetFlags flags) { if (flags == 0 || (flags & UPDATE_OBSOLETE)) return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey(); + if (flags & (UPDATE_PARTIAL|UPDATE_PENDING)) + return ansi_highlight_cyan(); + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_INCOMPLETE)) return ansi_highlight_yellow(); @@ -29,6 +32,9 @@ const char* update_set_flags_to_glyph(UpdateSetFlags flags) { if (flags == 0 || (flags & UPDATE_OBSOLETE)) return glyph(GLYPH_MULTIPLICATION_SIGN); + if (flags & (UPDATE_PARTIAL|UPDATE_PENDING)) + return glyph(GLYPH_DOWNLOAD); + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST)) return glyph(GLYPH_BLACK_CIRCLE); @@ -54,6 +60,18 @@ const char* update_set_flags_to_string(UpdateSetFlags flags) { case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: return "current"; + case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current+pending"; + + case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current+partial"; + case UPDATE_AVAILABLE|UPDATE_NEWEST: case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: return "candidate"; diff --git a/src/sysupdate/sysupdate-update-set-flags.h b/src/sysupdate/sysupdate-update-set-flags.h index 27cfab8c7c..fd4e77e117 100644 --- a/src/sysupdate/sysupdate-update-set-flags.h +++ b/src/sysupdate/sysupdate-update-set-flags.h @@ -10,6 +10,8 @@ typedef enum UpdateSetFlags { UPDATE_OBSOLETE = 1 << 3, UPDATE_PROTECTED = 1 << 4, UPDATE_INCOMPLETE = 1 << 5, + UPDATE_PARTIAL = 1 << 6, + UPDATE_PENDING = 1 << 7, } UpdateSetFlags; const char* update_set_flags_to_color(UpdateSetFlags flags); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index e1143ad4ce..a40e94da6c 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -595,13 +595,14 @@ static int context_show_version(Context *c, const char *version) { if (!sd_json_format_enabled(arg_json_format_flags)) printf("%s%s%s Version: %s\n" " State: %s%s%s\n" - "Installed: %s%s\n" + "Installed: %s%s%s%s\n" "Available: %s%s\n" "Protected: %s%s%s\n" " Obsolete: %s%s%s\n\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)" : "", + FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "", yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "", 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()); @@ -794,7 +795,7 @@ static int context_show_version(Context *c, const char *version) { if (!sd_json_format_enabled(arg_json_format_flags)) { printf("%s%s%s Version: %s\n" " State: %s%s%s\n" - "Installed: %s%s%s%s%s\n" + "Installed: %s%s%s%s%s%s%s\n" "Available: %s%s\n" "Protected: %s%s%s\n" " Obsolete: %s%s%s\n", @@ -802,6 +803,7 @@ static int context_show_version(Context *c, const char *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)" : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(), + FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "", yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "", 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()); @@ -827,6 +829,8 @@ static int context_show_version(Context *c, const char *version) { SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)), SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)), SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)), + SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)), + SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)), 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_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)), @@ -1193,13 +1197,16 @@ static int verb_list(int argc, char **argv, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; _cleanup_strv_free_ char **versions = NULL; const char *current = NULL; + bool current_is_pending = false; 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)) + FLAGS_SET(us->flags, UPDATE_NEWEST)) { current = us->version; + current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING); + } r = strv_extend(&versions, us->version); if (r < 0) @@ -1217,7 +1224,7 @@ static int verb_list(int argc, char **argv, void *userdata) { return log_oom(); } - r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("current", current), + r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current), SD_JSON_BUILD_PAIR_STRV("all", versions), SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls)); if (r < 0) From cf7cccf2f7949736e47dd035d022554cd6f1609e Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:46:25 +0000 Subject: [PATCH 08/11] sysupdate: Split the update verb into two parts internally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ‘acquire’ (download) part, and an ‘install’ (apply) part. Following commits will expose these as separate verbs and D-Bus methods, but this commit is the one which rearranges the internals. If doing an ‘install’, a mirror version of the ‘acquire’ has to happen first to make sure the transfer’s internal state is correct. ‘Acquire’ can require an internet connection, but ‘install’ will always work with `--offline` specified. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- src/sysupdate/sysupdate-partition.c | 27 +++++++ src/sysupdate/sysupdate-partition.h | 1 + src/sysupdate/sysupdate-transfer.c | 36 +++++++++ src/sysupdate/sysupdate-transfer.h | 1 + src/sysupdate/sysupdate.c | 121 +++++++++++++++++++++++++--- 5 files changed, 177 insertions(+), 9 deletions(-) diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c index cd85db5236..237c157831 100644 --- a/src/sysupdate/sysupdate-partition.c +++ b/src/sysupdate/sysupdate-partition.c @@ -16,6 +16,33 @@ void partition_info_destroy(PartitionInfo *p) { p->device = mfree(p->device); } +int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src) { + int r; + + assert(dest); + assert(src); + + r = free_and_strdup_warn(&dest->label, src->label); + if (r < 0) + return r; + + r = free_and_strdup_warn(&dest->device, src->device); + if (r < 0) + return r; + + dest->partno = src->partno; + dest->start = src->start; + dest->size = src->size; + dest->flags = src->flags; + dest->type = src->type; + dest->uuid = src->uuid; + dest->no_auto = src->no_auto; + dest->read_only = src->read_only; + dest->growfs = src->growfs; + + return 0; +} + int read_partition_info( struct fdisk_context *c, struct fdisk_table *t, diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h index 21277962e4..fbda14de6f 100644 --- a/src/sysupdate/sysupdate-partition.h +++ b/src/sysupdate/sysupdate-partition.h @@ -36,6 +36,7 @@ typedef struct PartitionInfo { } void partition_info_destroy(PartitionInfo *p); +int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src); int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret); diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 5af2f4cf30..a13f659e36 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -1599,6 +1599,42 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi return 0; } +int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i) { + InstanceMetadata f; + Instance *existing; + int r; + + assert(t); + assert(i); + + log_debug("transfer_process_partial_and_pending_instance %s", i->path); + + /* Does this instance already exist in the target but isn’t pending? */ + existing = resource_find_instance(&t->target, i->metadata.version); + if (existing && !existing->is_pending) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already in the target but is not pending.", i->path); + + /* All we need to do is compute the temporary paths. We don’t need to do any of the other work in + * transfer_acquire_instance(). */ + r = transfer_compute_temporary_paths(t, i, &f); + if (r < 0) + return r; + + /* This is the analogue of find_suitable_partition(), but since finding the suitable partition has + * already happened in the acquire phase, the target should already have that information and it + * should already have been claimed as `PND#`. */ + if (t->target.type == RESOURCE_PARTITION) { + assert(i->resource == &t->target); + assert(i->is_pending); + + r = partition_info_copy(&t->partition_info, &i->partition_info); + if (r < 0) + return r; + } + + return 0; +} + int transfer_install_instance( Transfer *t, Instance *i, diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h index 92c1922795..f5976f0477 100644 --- a/src/sysupdate/sysupdate-transfer.h +++ b/src/sysupdate/sysupdate-transfer.h @@ -67,5 +67,6 @@ int transfer_resolve_paths(Transfer *t, const char *root, const char *node); int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version); int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata); +int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i); int transfer_install_instance(Transfer *t, Instance *i, const char *root); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index a40e94da6c..700962819f 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -397,7 +397,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags assert(flags == UPDATE_INSTALLED); match = resource_find_instance(&t->target, cursor); - if (!match) { + if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING))) { /* When we're looking for installed versions, let's be robust and treat * an incomplete installation as an installation. Otherwise, there are * situations that can lead to sysupdate wiping the currently booted OS. @@ -413,6 +413,14 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags if (strv_contains(t->protected_versions, cursor)) extra_flags |= UPDATE_PROTECTED; + + /* Partial or pending updates by definition are not incomplete, they’re + * partial/pending instead */ + if (match && match->is_partial) + extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE; + + if (match && match->is_pending) + extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE; } r = free_and_strdup_warn(&boundary, cursor); @@ -431,7 +439,9 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags /* Merge in what we've learned and continue onto the next version */ - if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE)) { + if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) || + FLAGS_SET(u->flags, UPDATE_PARTIAL) || + FLAGS_SET(u->flags, UPDATE_PENDING)) { assert(u->n_instances == c->n_transfers); /* Incomplete updates will have picked NULL instances for the transfers that @@ -450,7 +460,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags /* If this is the newest installed version, that is incomplete and just became marked * as available, and if there is no other candidate available, we promote this to be - * the candidate. */ + * the candidate. Ignore partial or pending status on the update set. */ if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) && !c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE)) c->candidate = u; @@ -486,7 +496,8 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED)) c->newest_installed = us; - /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */ + /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate". + * It may be partial or pending. */ if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE)) c->candidate = us; } @@ -496,6 +507,11 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0) c->candidate = NULL; + /* Newest installed is still pending and no candidate is set? Then it becomes the candidate. */ + if (c->newest_installed && FLAGS_SET(c->newest_installed->flags, UPDATE_PENDING) && + !c->candidate) + c->candidate = c->newest_installed; + return 0; } @@ -986,6 +1002,8 @@ static int context_on_acquire_progress(const Transfer *t, const Instance *inst, overall, n - i, i, inst->metadata.version, overall); } +static int context_process_partial_and_pending(Context *c, const char *version); + static int context_acquire( Context *c, const char *version) { @@ -1011,7 +1029,15 @@ static int context_acquire( if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE)) log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version); - else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { + else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) { + log_info("Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version); + + return 0; + } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) { + log_info("Selected update '%s' is already acquired and pending installation.", us->version); + + return context_process_partial_and_pending(c, version); + } else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { log_info("Selected update '%s' is already installed. Skipping update.", us->version); return 0; @@ -1075,6 +1101,70 @@ static int context_acquire( return 1; } +/* Check to see if we have an update set acquired and pending installation. */ +static int context_process_partial_and_pending( + Context *c, + const char *version) { + + UpdateSet *us = NULL; + int r; + + assert(c); + + if (version) { + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + } else { + if (!c->candidate) { + log_info("No update needed."); + + return 0; + } + + us = c->candidate; + } + + if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE)) + log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version); + else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) { + log_info("Selected update '%s' is already installed. Skipping update.", us->version); + + return 0; + } + + if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version); + if (!FLAGS_SET(us->flags, UPDATE_PENDING)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version); + + if (FLAGS_SET(us->flags, UPDATE_OBSOLETE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version); + + if (!FLAGS_SET(us->flags, UPDATE_NEWEST)) + log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version); + if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0) + log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version); + + log_info("Selected update '%s' for install.", us->version); + + /* There should now be one instance picked for each transfer, and the order is the same */ + assert(us->n_instances == c->n_transfers); + + for (size_t i = 0; i < c->n_transfers; i++) { + Instance *inst = us->instances[i]; + Transfer *t = c->transfers[i]; + + assert(inst); + + r = transfer_process_partial_and_pending_instance(t, inst); + if (r < 0) + return r; + } + + return 1; +} + static int context_install( Context *c, const char *version, @@ -1106,7 +1196,8 @@ static int context_install( Instance *inst = us->instances[i]; Transfer *t = c->transfers[i]; - if (inst->resource == &t->target) + if (inst->resource == &t->target && + !inst->is_pending) continue; r = transfer_install_instance(t, inst, arg_root); @@ -1436,7 +1527,12 @@ static int verb_vacuum(int argc, char **argv, void *userdata) { return context_vacuum(context, 0, NULL); } -static int verb_update(int argc, char **argv, void *userdata) { +typedef enum { + UPDATE_ACTION_ACQUIRE = 1 << 0, + UPDATE_ACTION_INSTALL = 1 << 1, +} UpdateActionFlags; + +static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; _cleanup_(context_freep) Context* context = NULL; @@ -1470,11 +1566,14 @@ static int verb_update(int argc, char **argv, void *userdata) { if (r < 0) return r; - r = context_acquire(context, version); + if (action_flags & UPDATE_ACTION_ACQUIRE) + r = context_acquire(context, version); + else + r = context_process_partial_and_pending(context, version); if (r < 0) return r; /* error */ - if (r > 0) /* update needed */ + if (action_flags & UPDATE_ACTION_INSTALL && r > 0) /* update needed */ r = context_install(context, version, &applied); if (r < 0) return r; @@ -1500,6 +1599,10 @@ static int verb_update(int argc, char **argv, void *userdata) { return 0; } +static int verb_update(int argc, char **argv, void *userdata) { + return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE | UPDATE_ACTION_INSTALL); +} + static int verb_pending_or_reboot(int argc, char **argv, void *userdata) { _cleanup_(context_freep) Context* context = NULL; _cleanup_free_ char *booted_version = NULL; From 3bc92084357c210c6961e028607764abfde10a33 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 31 Dec 2025 00:48:54 +0000 Subject: [PATCH 09/11] sysupdate: Add acquire and install verbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These expose the two parts of ‘update’, so that update sets can be acquired (downloaded) and installed (applied) in separate actions at different times. For example, this could allow a load of update sets to be acquired when online, and later applied when offline. Signed-off-by: Philip Withnall Helps: https://github.com/systemd/systemd/issues/34814 --- man/systemd-sysupdate.xml | 23 ++++++++++++++++++++--- src/sysupdate/sysupdate.c | 13 ++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index 778ff2e793..1dea457e13 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -128,19 +128,33 @@ - VERSION + --offline VERSION Installs (updates to) the specified version, or if none is specified to the newest version available. If the version is already installed or no newer version available, no operation is executed. + If is specified, the update must already have been acquired using + acquire and, if so, this pre-acquired version is the one which will be updated + to. + + + + + + VERSION + + Acquires (downloads) the specified version, ready to install it. If no version is + specified, the newest version available is acquired. If the version is already installed or no newer + version is available, no operation is executed. + If a new version to install/update to is found, old installed versions are deleted until at least one new version can be installed, as configured via InstanceMax= in sysupdate.d5, or via the available partition slots of the right type. This implicit operation can also be invoked explicitly via the vacuum command described below. - + @@ -299,7 +313,7 @@ - When used in combination with the update command and a new version is + When used in combination with the update commands and a new version is installed, automatically reboots the system immediately afterwards. @@ -312,6 +326,9 @@ This is most useful when used in combination with the list command, to query locally installed versions. + If used in combination with the update command, it allows updates to be + downloaded in advance (using acquire) and installed later. + diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 700962819f..9af09b497d 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -1600,7 +1600,16 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag } static int verb_update(int argc, char **argv, void *userdata) { - return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE | UPDATE_ACTION_INSTALL); + UpdateActionFlags flags = UPDATE_ACTION_INSTALL; + + if (!arg_offline) + flags |= UPDATE_ACTION_ACQUIRE; + + return verb_update_impl(argc, argv, flags); +} + +static int verb_acquire(int argc, char **argv, void *userdata) { + return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE); } static int verb_pending_or_reboot(int argc, char **argv, void *userdata) { @@ -1782,6 +1791,7 @@ static int verb_help(int argc, char **argv, void *userdata) { " features [FEATURE] Show optional features\n" " check-new Check if there's a new version available\n" " update [VERSION] Install new version now\n" + " acquire [VERSION] Acquire (download) new version now\n" " vacuum Make room, by deleting old versions\n" " pending Report whether a newer version is installed than\n" " currently booted\n" @@ -1995,6 +2005,7 @@ static int sysupdate_main(int argc, char *argv[]) { { "features", VERB_ANY, 2, 0, verb_features }, { "check-new", VERB_ANY, 1, 0, verb_check_new }, { "update", VERB_ANY, 2, 0, verb_update }, + { "acquire", VERB_ANY, 2, 0, verb_acquire }, { "vacuum", VERB_ANY, 1, 0, verb_vacuum }, { "reboot", 1, 1, 0, verb_pending_or_reboot }, { "pending", 1, 1, 0, verb_pending_or_reboot }, From e6e8efcffb91f1e6bbcda5987472bf68562db189 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 12 Jan 2026 16:43:20 +0000 Subject: [PATCH 10/11] sysupdate: Add some more debug output Signed-off-by: Philip Withnall --- src/sysupdate/sysupdate-transfer.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index a13f659e36..04b1fd09cd 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -1647,6 +1647,8 @@ int transfer_install_instance( assert(i->resource); assert(i->is_pending || t == container_of(i->resource, Transfer, source)); + log_debug("transfer_install_instance %s %s %s %d", i->path, t->temporary_pending_path, t->final_partition_label, t->partition_change); + if (t->temporary_pending_path) { assert(RESOURCE_IS_FILESYSTEM(t->target.type)); assert(t->final_path); From 6015c6cd6f4d94ced9a67490dd09ce41a54ebfcb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 12 Jan 2026 16:43:46 +0000 Subject: [PATCH 11/11] test: Expand sysupdate test to cover split acquire/install updates This essentially means the sysupdate tests are now run twice: once with a monolithic update (`sysupdate update`) and once with a split update (`sysupdate acquire; sysupdate install`). Signed-off-by: Philip Withnall --- test/units/TEST-72-SYSUPDATE.sh | 39 ++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index a3e0e834bb..f3b712a6e6 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -92,11 +92,27 @@ new_version() { } update_now() { + local update_type="${1:?}" + # Update to newest version. First there should be an update ready, then we # do the update, and then there should not be any ready anymore + # + # The update can either be done monolithically (by calling the `update` + # verb) or split (`acquire` then `update`). Both options are allowed for + # most updates in the test suite, so the test suite can be run to test both + # modes. Some updates in the test suite need to be monolithic (e.g. when + # repairing an installation), so that can be overridden via the local. "$SYSUPDATE" --verify=no check-new - "$SYSUPDATE" --verify=no update + if [[ "$update_type" == "monolithic" ]]; then + "$SYSUPDATE" --verify=no update + elif [[ "$update_type" == "split-offline" ]]; then + "$SYSUPDATE" --verify=no acquire + "$SYSUPDATE" --verify=no update --offline + elif [[ "$update_type" == "split" ]]; then + "$SYSUPDATE" --verify=no acquire + "$SYSUPDATE" --verify=no update + fi (! "$SYSUPDATE" --verify=no check-new) } @@ -135,6 +151,7 @@ verify_version_current() { } for sector_size in "${SECTOR_SIZES[@]}"; do +for update_type in monolithic split-offline split; do # Disk size of: # - 1MB for GPT # - 4 partitions of 2048 sectors each @@ -267,18 +284,18 @@ EOF # Install initial version and verify new_version "$sector_size" v1 - update_now + update_now "$update_type" verify_version_current "$blockdev" "$sector_size" v1 1 # Create second version, update and verify that it is added new_version "$sector_size" v2 - update_now + update_now "$update_type" verify_version "$blockdev" "$sector_size" v1 1 verify_version_current "$blockdev" "$sector_size" v2 2 # Create third version, update and verify it replaced the first version new_version "$sector_size" v3 - update_now + update_now "$update_type" verify_version_current "$blockdev" "$sector_size" v3 1 verify_version "$blockdev" "$sector_size" v2 2 test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v1+3-0.efi" @@ -295,16 +312,17 @@ EOF # Create a fifth version, that's complete on the server side. We should # completely skip the incomplete v4 and install v5 instead. new_version "$sector_size" v5 - update_now + update_now "$update_type" verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 # Make the local installation of v5 incomplete by deleting a file, then make # sure that sysupdate still recognizes the installation and can complete it # in place + # Always do this as a monolithic update for the repair to work. rm -r "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d" "$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null - update_now + update_now "monolithic" "$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 @@ -316,7 +334,7 @@ EOF mkdir "$CONFIGDIR/optional.feature.d" echo -e "[Feature]\nEnabled=true" > "$CONFIGDIR/optional.feature.d/enable.conf" "$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null - update_now + update_now "monolithic" "$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 @@ -340,7 +358,7 @@ EOF updatectl update else # If no updatectl, gracefully fall back to systemd-sysupdate - update_now + update_now "$update_type" fi # User-facing updatectl returns 0 if there's no updates, so use the low-level # utility to make sure we did upgrade @@ -397,7 +415,7 @@ MatchPattern=dir-@v InstancesMax=3 EOF - update_now + update_now "$update_type" verify_version "$blockdev" "$sector_size" v6 1 verify_version_current "$blockdev" "$sector_size" v7 2 @@ -420,7 +438,7 @@ EOF # (what .transfer files were called before v257) for i in "$CONFIGDIR/"*.conf; do echo mv "$i" "${i%.conf}.transfer"; done new_version "$sector_size" v8 - update_now + update_now "$update_type" verify_version_current "$blockdev" "$sector_size" v8 1 verify_version "$blockdev" "$sector_size" v7 2 @@ -428,5 +446,6 @@ EOF [[ -b "$blockdev" ]] && losetup --detach "$blockdev" rm "$BACKING_FILE" done +done touch /testok